Skip to content

Agent Construction

The SDK exposes two construction entry points, and choosing the right one is the first important API decision.

Choose the builder

BuilderUse 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(...)

ts
function createSmartAgent<TOutput = unknown>(options: SmartAgentOptions): SmartAgentInstance<TOutput>

Key option groups

  • model: the model adapter used by the runtime
  • tools: local or adapted tools
  • runtimeProfile: built-in preset or custom
  • planning: explicit multi-step workflow control
  • summarization, context, toolResponses: context pressure handling
  • memory: fact read/write policy
  • delegation: child-agent behavior (depth, child-call budget, context policy — enforced at runtime)
  • skills, skillPolicy: progressive capability disclosure for large or optional tool catalogs
  • tracing: execution telemetry
  • outputSchema: deterministic structured output
  • limits: budget surfaces — see Limits & Tokens for the full list including maxTotalOutputTokens, maxCostUsd, maxWallClockMs
  • tokenCounter: drop-in tokenizer used by all internal estimates (default: character heuristic)
  • costEstimator: pluggable pricing function required for the maxCostUsd budget

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)

  • defaultPolicy is applied by the summarizer to non-critical tool messages. Valid values: keep_full, keep_structured, summarize_archive, drop. Default is summarize_archive.
  • retentionByTool[name].output is the per-tool override.
  • toolResponseRetentionByTool is the legacy single-axis map. Still fully honored (consulted right after retentionByTool) — prefer retentionByTool.
  • criticalTools is the set of tool names that are never reduced. The default list covers response, manage_todo_list, and get_tool_response.

Input axis (tool-call arguments)

  • defaultInputPolicykeep (default) or digest. keep means arguments are never touched, so argument compaction is strictly opt-in.
  • retentionByTool[name].input is the per-tool override; a tool can also declare its own default via createTool({ retention: { input: "digest" } }).
  • digest is field-level: only string fields longer than maxToolInputFieldChars (default 2000) are replaced by a {__digest:{chars,sha256,head,recover}} descriptor keeping maxToolInputDigestHeadChars (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

  • maxToolResponseChars and maxToolResponseTokens only drive an eager hard-cap truncation when a single non-critical tool output is oversized. Truncated heads always point at get_tool_response for recovery.
  • schemaValidation controls 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.inputdefaultInputPolicy.

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:

ts
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

ts
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.plan updates
  • built-in archived tool-response retrieval via get_tool_response

createAgent(...)

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

ts
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 pending ask_user_question entry (see humanInTheLoop.askUser below)
  • 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:

ts
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 error payload when delegation.mode === "off".
  • Tracks delegation depth via state.ctx.__delegationDepth and refuses further nesting past delegation.maxDelegationDepth.
  • Counts child invocations against delegation.maxChildCalls (shared across the invoke).
  • Seeds child messages according to delegation.childContextPolicy:
    • minimal — only the explicit delegation input
    • scoped — parent system + last user message + delegation input
    • full — 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(...)

ts
agent.invoke(state, config?)

Important InvokeConfig hooks:

  • onEvent(event) for tool, plan, trace, and handoff visibility
  • onStateChange(state) for pause and checkpoint workflows
  • checkpointReason to annotate why a snapshot was taken

Result shape

ts
type AgentInvokeResult<TOutput = unknown> = {
  content: string;
  output?: TOutput;
  messages: Message[];
  state?: SmartState;
  metadata?: { usage?: any };
}

State surfaces worth integrating

  • state.messages
  • state.toolHistory
  • state.toolHistoryArchived
  • state.plan
  • state.planVersion
  • state.summaryRecords
  • state.memoryFacts
  • state.pendingApprovals — populated when a tool with needsApproval: true is requested
  • state.pendingUserQuestions — populated when the model calls ask_user_question

If you are using the smart runtime, prefer state.plan over any event-only or legacy todo mental model.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub