Building Agents

This page walks through the four things almost every SDK agent combines: running the loop, human-in-the-loop approvals, custom tools, and playbooks. The full method and option tables are in the SDK Reference.

Run or stream

run() executes the loop to a terminal state and returns the result. stream() yields the same run as live events — text deltas, tool calls, plan and approval events:

const result = await CloudPeek.agent.run({
  input: "Triage the new alerts",
  instructions: "You are a security operations agent.",
  maxIterations: 25,
});
// result.status: "completed" | "failed" | "incomplete" | "requires_action"

for await (const event of CloudPeek.agent.stream({ input: "..." })) {
  // event.eventType / event.data
}

Approval gates only fire on stream() — the non-streaming path cannot pause for a human, so any agent with HITL must be driven through the stream.

Behaviour profiles (agents.md)

agentsMd layers a behaviour profile — tone, focus areas, workflow conventions — onto the system prompt. The SDK takes only the content; reading it is your concern (a local AGENTS.md file, a wiki page, a database row):

import { readFile } from "node:fs/promises";

await CloudPeek.agent.run({
  input: "Triage the new alerts",
  agentsMd: await readFile("AGENTS.md", "utf8"),
  instructions: "You are a security operations agent.",
});

The composed prompt has a strict priority order: CloudPeek's core rules first, then the agents.md profile, then instructions. The profile can shape how the agent works but never overrides safety, authorization, tenant isolation, or tool-use rules. Omitting it leaves the prompt exactly as before, and a run suspended for client-side tools keeps its profile across resume().

Human-in-the-loop

Pick a mode per run, then answer approval events:

const stream = CloudPeek.agent.stream({
  input: "Contain the compromised host",
  hitlMode: "external_write_only", // reads flow, writes wait for a human
  tools: [lookupTool, isolateTool],
});

for await (const event of stream) {
  if (event.eventType === "response.tool.approval_required") {
    const { approval_id, tool_name, tool_arguments } = event.data as {
      approval_id: string;
      tool_name: string;
      tool_arguments: Record<string, unknown>;
    };
    const ok = await askYourUser(tool_name, tool_arguments);
    ok ? CloudPeek.hitl.approve(approval_id) : CloudPeek.hitl.deny(approval_id);
  }
}

The rules that matter:

  • Modes. external_write_only gates external writes; external_only gates every external call regardless of metadata; approval_only gates everything; disabled gates nothing. These are the same modes as the CloudPeek app's HITL settings.
  • Fail-closed. A tool with no read-only declaration is treated as an external write and gated. Only a positive declaration skips the gate.
  • Only the initiating user can approve. approve/deny are scoped to the principal.userId that started the run; other callers are rejected and the approval stays pending.
  • Timeouts. Unanswered approvals auto-reject after five minutes, and the agent receives the rejection as context it can react to.
  • CloudPeek.hitl.pending() lists approvals the runtime is currently waiting on — enough to build an approval inbox.

Custom tools

In-process tools

Provide one executor callback at init, declare the tools per run so the LLM can see them:

await CloudPeek.init({
  database: ":memory:",
  principal: { tenantId: "local", userId: "operator" },
  executeDynamicTool: async (_session, toolId, _method, params) => {
    if (toolId === "lookup_asset") {
      return { result: await cmdb.lookup(params.hostname), success: true };
    }
    return { result: `Unknown tool: ${toolId}`, success: false };
  },
});

await CloudPeek.agent.run({
  input: "Is web-01 patched?",
  tools: [
    {
      type: "function",
      name: "lookup_asset",
      description: "Look up an asset in the CMDB",
      parameters: {
        type: "object",
        properties: { hostname: { type: "string" } },
      },
      isReadOnly: true,
    },
  ],
});

isReadOnly: true is your declaration that the tool has no side effects — in external_write_only mode it then skips write approval. Omit it and the tool is gated. Because your application is the operator, its declaration about its own tools is trusted.

Client-side tools

If a tool must run somewhere the SDK isn't — an analyst's laptop, a browser — declare it but provide no executor. The run suspends with requires_action and the pending calls; execute them on your side and resume:

let result = await CloudPeek.agent.run({
  input: "...",
  tools: [BASH_TOOL],
  runId: "job-7",
});

while (result.status === "requires_action") {
  const outputs = await Promise.all(
    result.pendingToolCalls!.map(async (call) => ({
      callId: call.call_id,
      output: await runOnClient(call.name, JSON.parse(call.arguments)),
    })),
  );
  result = await CloudPeek.agent.resume("job-7", outputs);
}

The handoff works from stream() too — the suspension is recorded from the stream's requires-action event, so a streaming UI can resume the same way.

Playbooks

Give the runtime a playbook source and the agent's built-in playbook_list and playbook_execute tools light up. Any object with search(tenantId, opts) and get(tenantId, id) works — a directory of markdown files, a database, or CloudPeek's own runbooks via a wiki-backed provider:

await CloudPeek.init({
  database: ":memory:",
  principal: { tenantId: "local" },
  runbookProvider: myPlaybookSource,
});

await CloudPeek.agent.run({
  input: "We have a phishing report",
  instructions:
    "Check playbook_list for a matching playbook before acting, and follow it when one exists.",
});

Without a provider, the playbook tools respond with a structured "not configured" message rather than failing the run.

Observing runs

Subscribe to lifecycle events — iterations, tool calls with redacted arguments, compaction, completion — for logging or metrics:

const off = CloudPeek.on("*", (event) => log.info(event));
// later: off();

And inspect durable state through CloudPeek.sessions — each run records a session you can get, list, or check status on, always scoped to the calling principal's tenant.

© 2026 CloudPeek. Agentic AI for high-consequence security operations.