Agent Construction
The SDK exposes two construction entry points, and choosing the right one is the first important API decision.
Choose the builder
| Builder | Use it when... |
|---|---|
createAgent(...) | you want the minimal loop and will own prompts, planning, and context behavior yourself |
createSmartAgent(...) | you are building an autonomous or semi-autonomous agent that needs profiles, planning, summarization, memory, and better operational defaults |
createSmartAgent(...)
function createSmartAgent<TOutput = unknown>(options: SmartAgentOptions): SmartAgentInstance<TOutput>Key option groups
model: the model adapter used by the runtimetools: local or adapted toolsruntimeProfile: built-in preset orcustomplanning: explicit multi-step workflow controlsummarization,context,toolResponses: context pressure handlingmemory: fact read/write policydelegation: child-agent behavior (depth, child-call budget, context policy — enforced at runtime)skills,skillPolicy: progressive capability disclosure for large or optional tool catalogstracing: execution telemetryoutputSchema: deterministic structured outputlimits: budget surfaces — see Limits & Tokens for the full list includingmaxTotalOutputTokens,maxCostUsd,maxWallClockMstokenCounter: drop-in tokenizer used by all internal estimates (default: character heuristic)costEstimator: pluggable pricing function required for themaxCostUsdbudget
Tool response retention
toolResponses controls how tool payloads are stored in history and re-presented to the model under context pressure. Retention is lazy: tool inputs and outputs are stored in full in toolHistory and are only rewritten in the model view when the summarizer runs.
Retention has two independent axes, because value density is per-tool: a search tool carries a short query and returns the bulk, while a file writer carries the payload in its arguments and returns {ok: true}. Compacting only responses frees nothing on the second kind.
Output axis (tool results)
defaultPolicyis applied by the summarizer to non-critical tool messages. Valid values:keep_full,keep_structured,summarize_archive,drop. Default issummarize_archive.retentionByTool[name].outputis the per-tool override.toolResponseRetentionByToolis the legacy single-axis map. Still fully honored (consulted right afterretentionByTool) — preferretentionByTool.criticalToolsis the set of tool names that are never reduced. The default list coversresponse,manage_todo_list, andget_tool_response.
Input axis (tool-call arguments)
defaultInputPolicy—keep(default) ordigest.keepmeans arguments are never touched, so argument compaction is strictly opt-in.retentionByTool[name].inputis the per-tool override; a tool can also declare its own default viacreateTool({ retention: { input: "digest" } }).digestis field-level: only string fields longer thanmaxToolInputFieldChars(default 2000) are replaced by a{__digest:{chars,sha256,head,recover}}descriptor keepingmaxToolInputDigestHeadChars(default 200) leading characters. Identifying scalars — paths, ids, modes, indexes — always survive.- Control-plane and delegation tools (
response,manage_todo_list,ask_user_question,open_skill,bind_skill_tools,search_skills,get_tool_response,delegate_to,spawn_subagent,spawn_subagents_parallel) are never digested, and config cannot override that.
Shared
maxToolResponseCharsandmaxToolResponseTokensonly drive an eager hard-cap truncation when a single non-critical tool output is oversized. Truncated heads always point atget_tool_responsefor recovery.schemaValidationcontrols whether Zod-based tool input validation fails fast or only warns.
Resolution order at summarization time — output: critical tool → retentionByTool.output → legacy per-tool map → tool-declared retention.output → control-plane default → defaultPolicy. Input: control-plane/delegation → critical tool → retentionByTool.input → tool-declared retention.input → defaultInputPolicy.
Both sides remain recoverable through get_tool_response using the execution id embedded in the placeholder or digest: part: "output" (default) returns the archived result, part: "input" returns the original arguments.
Skills and progressive disclosure
createSmartAgent(...) accepts a skills catalog when the agent has many possible capabilities but only needs a small subset per task:
const agent = createSmartAgent({
model,
skills: [pdfSkill, jiraSkill],
skillPolicy: {
maxOpenSkills: 2,
maxBoundToolsPerSkill: 8,
maxBoundToolsTotal: 24,
modelTier: "large",
},
});When skills is non-empty, the smart runtime injects an <available_skills> header block and registers open_skill / bind_skill_tools. Small skills bind all tools when opened; large skills return a tool index and bind only selected tools.
Use direct tools for always-on primitives and skills for optional, large, or integration-scoped capability bundles. See Guide → Skills and API → Skills for the full contract.
Example
import { createSmartAgent, createTool, fromLangchainModel } from "@cognipeer/agent-sdk";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
const lookup = createTool({
name: "lookup_project",
description: "Return project facts",
schema: z.object({ code: z.string() }),
func: async ({ code }) => ({ code, owner: "Ada Lovelace", risk: "low" }),
});
const agent = createSmartAgent({
name: "ProjectAssistant",
model: fromLangchainModel(new ChatOpenAI({ model: "gpt-4o-mini" })),
tools: [lookup],
runtimeProfile: "balanced",
planning: { mode: "todo" },
toolResponses: {
defaultPolicy: "summarize_archive",
retentionByTool: { lookup: { output: "keep_full" } },
},
limits: { maxToolCalls: 8, maxContextTokens: 12000 },
tracing: { enabled: true },
});Why smart runtime users care
createSmartAgent(...) is the entry point you usually want for autonomous agents because it manages:
- adaptive planning
- model-facing context shaping
- summarization and archival
- memory fact sync
- canonical
state.planupdates - built-in archived tool-response retrieval via
get_tool_response
createAgent(...)
function createAgent<TOutput = unknown>(options: AgentOptions): AgentInstance<TOutput>Use this when you want the smallest deterministic loop and do not want smart runtime behavior to wrap the model call.
const agent = createAgent({
model,
tools: [lookup],
limits: { maxToolCalls: 4 },
});createAgent(...) still supports tools, approvals, handoffs, tracing, and structured output. It simply leaves planning and context strategy up to you.
Unlike createSmartAgent(...), the base builder does not automatically register get_tool_response. If you plan to archive or drop tool outputs in a base agent, provide your own retrieval strategy or keep those outputs inline.
Shared instance methods
Both builders expose more than invoke(...):
invoke(state, config?)snapshot(state, options?)resume(snapshot, options?)resolveToolApproval(state, resolution)resolveUserQuestion(state, resolution)— apply an answer to a pendingask_user_questionentry (seehumanInTheLoop.askUserbelow)asTool(options?)asHandoff(options?)
These methods matter if your agent is long-running, approval-gated, resumable, or composed into a bigger agent system.
humanInTheLoop.askUser
Both createAgent and createSmartAgent accept a humanInTheLoop option:
humanInTheLoop?: {
askUser?: boolean | {
allowFreeText?: boolean; // default true
promptOverride?: string; // override the built-in tool description
onQuestion?: (event: UserQuestionEvent) => void;
};
};When askUser is truthy the runtime registers a built-in ask_user_question tool. When the model calls it, the run pauses with state.pendingUserQuestions[0] populated and ctx.__awaitingUserQuestion set. Resume by calling agent.resolveUserQuestion(state, { id, answers }) and re-invoking. See Guide → Ask User for the full UX flow.
allowFreeText is a global decision: when false, the tool description tells the model that "Other" / typed answers are unavailable, every question must include >= 2 options, and the resolver rejects any freeText field on incoming answers.
asTool(...) and delegation
child.asTool({ toolName, description?, inputDescription? }) wraps an agent as a tool callable by another agent. When the wrapped tool runs, the runtime:
- Reads the parent's resolved delegation policy from the live runtime.
- Refuses the call with an
errorpayload whendelegation.mode === "off". - Tracks delegation depth via
state.ctx.__delegationDepthand refuses further nesting pastdelegation.maxDelegationDepth. - Counts child invocations against
delegation.maxChildCalls(shared across the invoke). - Seeds child messages according to
delegation.childContextPolicy:minimal— only the explicit delegation inputscoped— parent system + last user message + delegation inputfull— full parent transcript + delegation input
This makes asTool safe to expose to a model: nested delegation cannot recurse infinitely and the child does not inherit the full parent context unless you ask for it.
invoke(...)
agent.invoke(state, config?)Important InvokeConfig hooks:
onEvent(event)for tool, plan, trace, and handoff visibilityonStateChange(state)for pause and checkpoint workflowscheckpointReasonto annotate why a snapshot was taken
Result shape
type AgentInvokeResult<TOutput = unknown> = {
content: string;
output?: TOutput;
messages: Message[];
state?: SmartState;
metadata?: { usage?: any };
}State surfaces worth integrating
state.messagesstate.toolHistorystate.toolHistoryArchivedstate.planstate.planVersionstate.summaryRecordsstate.memoryFactsstate.pendingApprovals— populated when a tool withneedsApproval: trueis requestedstate.pendingUserQuestions— populated when the model callsask_user_question
If you are using the smart runtime, prefer state.plan over any event-only or legacy todo mental model.

