@kindgi/agents
npm install @kindgi/agents · source
Agent definitions and the agent-turn executor for Kindgi. defineAgent validates a declarative agent (instructions template, required model capabilities, versioned tool references, retrieval intents, guardrail ids, budgets, conversation and HITL policy) into a plain, serializable Agent. invokeAgent runs one turn of a conversation as a run of a built-in flow. Storage, model providers, tools, and the flow runtime are all supplied through binding interfaces, which a runtime adapter implements.
Purpose
Section titled “Purpose”Keep an agent a piece of data that can be versioned, stored, and pinned by { id, version }, and keep every side effect behind an interface. Because a turn is a journaled flow run, it can park on a human approval and resume later with the same run id, conversation, and provenance record.
Exports
Section titled “Exports”- Defining agents
defineAgent(spec: DefineAgentSpec)— returnsResult<Agent, InvalidAgentError>, collecting every validation issue with apathandmessage.DefineAgentSpechas the fields ofAgentexceptpreferredModel, withidandversionas plain strings.Agent—id(AgentId),version(exact semver),name,description?,instructions(a LiquidJS template),parameters?(PromptParameter[]),capabilities(Capability[]from@kindgi/capabilities; a turn routes on the first entry),tools,retrieval,guardrails(guardrail ids),preferredProvider?,preferredModel?,conversationPolicy?,budget?,tags?.ToolRef—{ id, version }, whereversionis an npm-style semver range ('1.2.3','^1.2.3','~1.2.3','>=1.0.0 <2.0.0') resolved at run start against the tenant's tool registry (ToolRegistry.forTenant). Bare tool ids are rejected.RetrievalIntent—types,scope(same-conversation,same-project,tenant),limit?(default 10),mode?(keyword,semantic,both; omitted lists the latest facts of each type).ConversationPolicy—historyLimit?(prior messages loaded into the prompt),autoCloseAfterInactiveSeconds?,hitlAfterTurns?, andhitl?: a session gate (afterTurns), per-tool gates (tools.defaultandtools.overrideswith modesnever_ask,ask_on_first_use,always_askand an optionalrequiredRole),defaultReviewerRole, andtimeoutMs. Tool gates apply only whenhitl.toolsis set.TurnBudget—maxSteps(default 8),maxCostUsd,maxWallMs(default 120 000).
PromptParameter,AgentId,ConversationId(re-exported from@kindgi/types).createAgentRegistry(seed?)— an in-memoryAgentRegistrykeyed by(id, version):register,get(id, version?),getLatest,list,listVersions,unregister.
- Prompt rendering
renderInstructions(agent, context: RenderContext)— rendersinstructionsin LiquidJS strict mode and returns{ ok: true, value: RenderResult }or{ ok: false, error: PromptRenderError }. A missing required parameter or unresolved variable is amissing-parametererror (MissingParameterError), never an empty string; other failures arerender-failure(RenderFailureError).RenderResult.contextholds every value the template saw.AUTO_INJECTED_VARS—today,now,agent(id,name,version),conversation(id,turn). These names are reserved and cannot be declared as parameters.
- Running turns
invokeAgent(input: InvokeAgentInput, bindings: InvokeAgentBindings)— returnsPromise<Result<AgentTurnResult, InvokeAgentError>>.InvokeAgentInput—tenantId,projectId,agent,conversationId(of a conversation opened with the same agent version),userMessage, and optionalparameters,participantId,abortSignal,dryRun(no conversation, memory or provenance writes; the model call is skipped),principalandauthz(when both are set, tool calls are authorized against the principal; see@kindgi/authz).InvokeAgentBindings—providerRegistry,toolRegistry(the turn resolves tools ontoolRegistry.forTenant(tenantId), so it never sees another tenant's tools),memoryBinding(aMemoryQueryBindingfrom@kindgi/memory),conversationBinding,runSnapshotBinding, andrunBinding(aRunBindingfrom@kindgi/runtime; optional in the type, required at call time); optionallytenantPolicy,policyRegistry,embeddingRegistry,embeddingModel,onEvent,provenance,hitl,resolveSecret, and theGuardrailsBindingsfields (guardrails,checks,compliance).tenantPolicyand the policy derived frompolicyRegistryare merged so the result is at least as strict as each (allow lists intersect, deny lists union, the smaller cap wins); the merged policy governs model routing and the models llm-judge guardrails may use.AgentTurnResult—conversationId,turnNumber,appended,response,retrieved,violations(failed guardrails whose action is nothalt— reported, not carried out),usage(AgentTurnUsage),provider, and optionalprovenance,dryRun,status.status: 'suspended'means the run parked on a HITL approval; the response fields are placeholders until it resumes.
resumeAgentTurn(input: ResumeAgentTurnInput, bindings)— continues a suspended turn after its approval resolves. The caller passes theAgentat the version the run started with.- It rebuilds the turn input from the run snapshot, and the turn's state from the run's journal and the conversation. That state is the environment
setupresolved, routed to the same provider and model, plus the messages the turn stored, its retrieved facts and its usage. - The turn goes on from the step it parked in. Calls that ran before the park keep their stored results.
- Provenance recorded before the park isn't rebuilt.
- Errors:
run-snapshot-missingwhen no snapshot was stored;run-journal-unavailablewhen the journal can't be read;capability-routing-failedwhen the turn's provider or model is no longer registered or allowed.
- It rebuilds the turn input from the run snapshot, and the turn's state from the run's journal and the conversation. That state is the environment
InvokeAgentError— theAgentErrorvariants plusUnresolvedToolError,tool-version-unresolvable,CapabilityRoutingError,ModelInvocationError,ToolInvocationError,BudgetExceededError(kind:stepsorcost),AgentTurnAbortedError(reason:external, ortimeoutwhenmaxWallMselapses),HitlRequiredError,GuardrailViolationError,UnresolvedGuardrailError,TenantPolicyUnavailableError(code: 'tenant-policy-unavailable', with thepolicyKind: a tenant policy the turn must apply could not be evaluated, so the turn fails rather than run without it).- Streaming —
bindings.onEvent(OnTurnEvent, sync or async) receives eachTurnEvent:turn.started,retrieval.completed,model.call.started,model.call.completed,tool.started,tool.completed,tool.failed,agent.message,guardrail.violated,turn.completed,turn.failed. Each has an exported type (TurnStartedEvent,ToolStartedEvent, …). Exceptions thrown by the handler are swallowed.
- Turn flow
AGENT_TURN_FLOW— the@kindgi/flowFlowevery turn runs, withAGENT_TURN_FLOW_ID('agent.turn'),AGENT_TURN_FLOW_VERSION('1.1.0'),AGENT_TURN_LOOP_MAX_ITERATIONS(32), and one*_NODEconstant per node id as it appears in run journals:setup→render-prompt→persist-user-message→run-retrievals→build-initial-messages→agent-loop(model-call→dispatch-tools→budget-check) →evaluate-guardrails→persist-final-message→persist-provenance→compose-result. Guardrails are evaluated once per turn, on the final response before it is stored; a failed check whose action ishaltfails the turn withguardrail-violation, and the response is never written to the conversation. Failures with any other action (log-only,retry,escalate,compensate, or a custom action — guardrail kinds and actions are open strings) are reported inviolationsand asguardrail.violatedevents; the turn does not carry them out.- Steps usable on their own:
runRetrievalswithRetrievalBindingsandformatRetrievedForPrompt(RetrievedFactpairs a fact with the intent that fetched it);resolveGuardrails,buildRunTrace,evaluateGate(takes the turn's tenant policy, under which llm-judge guardrails route),categorizeOutcomes,evaluateSessionGate(SessionGateResult) withGuardrailsBindings;persistProvenancewithProvenanceBindings(newBuilder,emit,emitBinding,keyProvider). resolveEffectiveHitlPolicy({ tenant, agent })— returns anEffectiveHitlPolicy: framework defaults (24 h timeout,standardreviewer,escalateon timeout) overlaid with the agent's policy, then held to the tenant'shitlpolicy (tenant: aHitlSpecfrom@kindgi/policy-contract, orundefined), which can only tighten: it caps the timeout, raises the reviewer role, and setstoolFloors— per tool, a gate is the stricter of the agent's and the tenant's. Each turn resolves it once, evaluatinghitlthroughpolicyRegistry; an evaluation that throws fails the turn withtenant-policy-unavailable.
- Bindings, implemented by a runtime adapter
ConversationBinding—openConversation,getConversation,listConversations,listConversationsPage(keyset pagination byopenedAt DESC, id DESCwith aConversationPageCursor),closeConversation,deleteConversation,appendMessage,readMessages. Inputs:OpenConversationInput,ListConversationsInput,ListConversationsPageInput,AppendMessageInput,ReadMessagesInput; results:Conversation,ConversationPage,ConversationMessage(MessageRole:user,agent,tool,system).RunSnapshotBinding—write(RunSnapshotWriteInput)andread(tenantId, runId)of theRunSnapshotRecordthatresumeAgentTurnneeds. Writes must be idempotent per run id.- Postgres support — Drizzle tables
agentConversationsandagentRunSnapshotswith their row types (AgentConversationRow,NewAgentConversationRow,AgentRunSnapshotRow,NewAgentRunSnapshotRow),AGENTS_TENANT_SCOPED_TABLES,AGENTS_MIGRATIONS_DIR(absolute path to the bundled SQL migrations), and the{ v, doc }JSONB envelope helperswrap,unwrap,unwrapOrThrow,CURRENT_AGENTS_PAYLOAD_VERSIONwithEnvelopeError,MalformedEnvelopeError,UnsupportedPayloadVersionError.
- Errors —
AgentError:InvalidAgentError,AgentNotFoundError,AgentAlreadyRegisteredError,AgentVersionMismatchError,ConversationNotFoundError,ConversationClosedError,InvalidMessageError,PersistenceError. Every error carries acode.
Example
Section titled “Example”import { defineAgent, invokeAgent } from '@kindgi/agents';import type { InvokeAgentBindings } from '@kindgi/agents';import type { ProjectId, TenantId } from '@kindgi/types';
const drafter = defineAgent({ id: 'acme.contract-drafter', version: '1.0.0', name: 'Contract Drafter', instructions: 'You draft contract clauses for {{ firmName }}. Today is {{ today }}.', parameters: [{ name: 'firmName', type: 'string' }], capabilities: [{ needs: [{ feature: 'tool-use' }] }], tools: [ { id: 'acme.clause-search', version: '^1.0.0' }, { id: 'acme.send-email', version: '~2.1.0' }, ], retrieval: [{ types: ['acme.clause'], scope: 'same-project', mode: 'keyword', limit: 5 }], guardrails: ['acme.no-pii'], conversationPolicy: { historyLimit: 20, hitl: { tools: { overrides: { 'acme.send-email': 'always_ask' } } }, }, budget: { maxSteps: 6, maxCostUsd: 0.25, maxWallMs: 60_000 },});if (drafter.kind === 'err') throw new Error(JSON.stringify(drafter.error.issues));const agent = drafter.value;
// `bindings` come from the runtime that hosts the agent, or from your own implementations.export async function firstTurn( bindings: InvokeAgentBindings, tenantId: TenantId, projectId: ProjectId, userMessage: string,): Promise<string | undefined> { const conversation = await bindings.conversationBinding.openConversation({ tenantId, agentId: agent.id, agentVersion: agent.version, title: 'NDA for globex', scope: { tenantId, projectId }, }); if (conversation.kind === 'err') throw new Error(conversation.error.message);
const result = await invokeAgent( { tenantId, projectId, agent, conversationId: conversation.value.id, userMessage, parameters: { firmName: 'Acme LLP' }, }, { ...bindings, onEvent: (event) => { if (event.kind === 'tool.started') console.log(`calling ${event.toolId}@${event.toolVersion}`); }, }, );
if (result.kind === 'err') { if (result.error.code === 'budget-exceeded') return `Stopped: ${result.error.kind} budget exceeded`; throw new Error(`${result.error.code}: ${result.error.message}`); } if (result.value.status === 'suspended') return undefined; // waiting for a reviewer; see resumeAgentTurn const { content } = result.value.response; return typeof content === 'string' ? content : JSON.stringify(content);}Non-goals
Section titled “Non-goals”- No storage or model access of its own. Conversations, messages, run snapshots, memory, provenance, providers, tools, and flow execution all go through the bindings; the package ships Drizzle tables and migrations for Postgres implementations but never opens a database connection.
- No tenancy in the agent registry.
AgentRegistryis a plain in-memory catalog; multi-tenant deployments keep one registry per tenant or layer isolation on top. - No moving a conversation to a new agent version. A conversation stays pinned to the agent version it was opened with; invoking it with another version fails with
agent-version-mismatch, and a closed conversation cannot be reopened.
Related
Section titled “Related”@kindgi/sdk— re-exportsdefineAgent,Agent, andDefineAgentSpecfrom@kindgi/sdk/define.@kindgi/capabilities— capability declarations and the router that picks a model for each turn.@kindgi/toolsand@kindgi/guardrails— tool and check definitions referenced bytoolsandguardrails.@kindgi/memoryand@kindgi/provenance— the memory binding used for retrieval, and the provenance record built for each turn.@kindgi/flowand@kindgi/runtime— the flow model and theRunBindingthat executes the agent-turn flow.