Deuz SDK
Core

generateText

Buffered, awaited text generation — single-turn, or the full agentic tool loop when you pass tools.

generateText runs a model to completion and resolves once. It is the buffered counterpart to streamChat: same orchestration (retry, timeout, the canonical delta stream), but it accumulates every delta for you and returns a plain object. Reach for it when you want the final answer in one await and don't need to render tokens as they arrive. Pass tools and it becomes the agentic loop — model step, tool execution, feed results back, repeat.

Signature

import { generateText } from '@deuz-sdk/core';

const result = await generateText(options);

generateText is async and returns a Promise<GenerateTextResult>. Unlike streamChat, errors reject the promise — wrap the call in try/catch.

Options

generateText takes the same CommonCallOptions as every other call. The most common:

OptionTypeDefaultNotes
modelLanguageModelFrom a provider factory, e.g. createAnthropic(...)('claude-opus-4-8').
messagesMessage[]Canonical message history.
temperaturenumberproviderSampling temperature.
maxOutputTokensnumberproviderCap on generated tokens.
topPnumberproviderNucleus sampling.
stopSequencesstring[]Hard stop strings.
effort'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max'Canonical reasoning effort; each adapter maps it to its own unit ('xhigh'/'max' clamp down on wires that lack them).
signalAbortSignalCancellation; propagated to the underlying fetch and to tool execute.
maxRetriesnumber2Pre-first-byte retries only.
headersRecord<string, string>Extra request headers.
onUsage(usage, meta) => voidPer-request usage callback.
onFinish(meta) => voidFires when the call settles.

Agentic options (only meaningful with tools):

OptionTypeDefaultNotes
toolsToolSetA Record<string, Tool>. Presence switches on the loop.
toolChoice'auto' | 'required' | 'none' | { type: 'tool'; toolName: string }'auto'Forces / forbids tool use.
maxStepsnumber1Max model turns in the loop.
stopWhenStopCondition | StopCondition[]Extra stop predicate(s), OR-ed with maxSteps.
maxToolConcurrencynumber5Max parallel tool executions per step.
onStepFinish(step: StepResult) => voidFires after each completed step.
prepareStep(ctx) => PrepareStepResult | undefined | Promise<…>Pre-step hook run before every model call (after compaction). Return per-step overrides to rewrite messages, swap the model, restrict activeTools, or override toolChoice for that step.
compaction'auto' | CompactionPolicyoffOpt-in automatic layered context compaction for the loop. Pruning is free; the summarize layer costs one extra model call whose usage counts toward the total. History stays immutable.
activeToolsstring[]Static tool filter: only these tools keys are sent to the model across all steps; unknown names log a warning and are ignored. prepareStep's activeTools overrides it per step.
approveToolCall(call, ctx) => boolean | Promise<boolean>Server-mode approval gate for needsApproval tools; return false (or throw) to deny — denials become an is_error tool result and the loop continues.
approvalResponsesToolApprovalResponse[]Resume after a client-mode break: verdicts for the trailing assistant turn's pending calls. Unmatched pending calls are denied by default.

Result shape

interface GenerateTextResult {
  text: string;
  usage: Usage;
  finishReason: FinishReason;
  response: { messages: Message[] };
  steps?: StepResult[];
  toolCalls?: ToolCall[];
  toolResults?: ToolResult[];
  pendingApprovals?: ToolApprovalRequest[];
}
FieldTypeWhenNotes
textstringalwaysFinal assistant text. With tools, this is the last step's text.
usageUsagealwaysToken usage summed across all steps.
finishReasonFinishReasonalwaysOne of 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error' | 'aborted'.
response.messagesMessage[]alwaysNew messages to append to history (assistant + tool turns across all steps).
stepsStepResult[]with toolsPer-step breakdown. undefined on a single-turn call (no tools).
toolCallsToolCall[]with toolsConvenience: the last tool-calling step's calls.
toolResultsToolResult[]with toolsConvenience: that step's results.
pendingApprovalsToolApprovalRequest[]client-mode approval breakCalls awaiting a verdict when a needsApproval tool broke the loop; resume by calling again with approvalResponses.

response.messages only contains the new turns this call produced — append them to your prior messages to continue the conversation.

Simple call

A single buffered turn. With no tools, steps/toolCalls/toolResults are absent.

generate.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const { text, usage, finishReason } = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Name three primary colors.' }],
});

console.log(text);
console.log(finishReason); // 'stop'
console.log(usage.totalTokens);

With tools (the agentic loop)

Add tools and generateText runs the loop: it calls the model, executes any tool calls in parallel (capped by maxToolConcurrency), feeds the results back as a new turn, and repeats until the model stops calling tools or a stop condition fires. maxSteps bounds the number of model turns — leave it at the default 1 and the loop runs a single turn, so for real tool use set it higher.

Tools are defined with a parameters schema (any Standard Schema such as Zod, or a raw JSON Schema) and an execute function. A thrown execute is caught and fed back to the model as an error tool result — it never rejects the promise.

weather.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { z } from 'zod';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const { text, steps } = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
  maxSteps: 5,
  tools: {
    getWeather: {
      description: 'Get the current weather for a city.',
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, tempC: 22, sky: 'sunny' }),
    },
  },
});

console.log(text);            // final natural-language answer
console.log(steps?.length);   // e.g. 2: one tool turn + one final turn

You can also pass a raw JSON Schema instead of Zod — no extra dependency required:

const tools = {
  getWeather: {
    description: 'Get the current weather for a city.',
    parameters: {
      type: 'object',
      properties: { city: { type: 'string' } },
      required: ['city'],
      additionalProperties: false,
    },
    execute: async (args: { city: string }) => ({ city: args.city, tempC: 22 }),
  },
} as const;

Step array anatomy

When tools are used, result.steps is a StepResult[] — one entry per model turn, in order. Each step:

interface StepResult {
  stepType: 'initial' | 'tool-result';
  text: string;
  reasoningText?: string;
  toolCalls: ToolCall[];
  toolResults: ToolResult[];
  finishReason: FinishReason;
  usage: Usage;
  response: { messages: Message[] };
}

The first step is 'initial'; subsequent steps (produced because the previous step's tool results were fed back) are 'tool-result'. A step that called no tools has empty toolCalls/toolResults and is the loop's last step. usage on each step is that step alone; result.usage is the sum.

Reading steps

import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { z } from 'zod';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const result = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Weather in Paris and Berlin?' }],
  maxSteps: 5,
  tools: {
    getWeather: {
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, tempC: 22 }),
    },
  },
  onStepFinish: (step) => {
    console.log(`[${step.stepType}] ${step.toolCalls.length} tool call(s)`);
  },
});

for (const step of result.steps ?? []) {
  for (const call of step.toolCalls) {
    console.log('called', call.toolName, 'with', call.args);
  }
  for (const r of step.toolResults) {
    console.log('result', r.toolName, r.isError ? '(error)' : '', r.result);
  }
}

Stopping early with stopWhen

stopWhen adds predicate(s) that are OR-ed with maxSteps. A StopCondition is a function over the loop state; return true to stop after the current step.

The SDK also ships four ready-made stop conditions you can import from @deuz-sdk/core instead of writing your own: stepCountIs(n), hasToolCall(name), totalTokensExceed(n), and costExceeds(usd). The last two read the real provider-reported usage accumulated across all steps; costExceeds needs deps.priceProvider injected (without one it warns once and never fires).

import { stepCountIs, totalTokensExceed } from '@deuz-sdk/core';

// Stop at 10 steps or once accumulated usage crosses 50k tokens.
const result = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Plan my trip.' }],
  stopWhen: [stepCountIs(10), totalTokensExceed(50_000)],
  tools: { /* … */ },
});

You can also write a custom StopCondition:

import type { StopCondition } from '@deuz-sdk/core';

// Stop as soon as a step has produced final text (no tool calls).
const untilFinalText: StopCondition = ({ steps }) =>
  (steps.at(-1)?.toolCalls.length ?? 0) === 0;

const result = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Plan my trip.' }],
  maxSteps: 8,
  stopWhen: untilFinalText,
  tools: { /* … */ },
});

Continuing a conversation

response.messages holds only the new turns. Append them to keep the history immutable across calls:

import type { Message } from '@deuz-sdk/core';

let messages: Message[] = [{ role: 'user', content: 'Weather in Paris?' }];

const first = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages,
  maxSteps: 5,
  tools: { /* … */ },
});

messages = [...messages, ...first.response.messages];
// next turn reuses the full, stable history (prompt-cache friendly)

Approving tool calls

A tool marked needsApproval is gated before it runs. There are two modes:

  • Server mode — pass approveToolCall. The loop calls it for each gated tool and runs the call only if it returns true; returning false (or throwing) denies it, and the denial is fed back as an is_error tool result so the loop continues.
  • Client mode — omit approveToolCall. A gated call breaks the loop and is returned in result.pendingApprovals. Collect verdicts from your user, then call generateText again with the same appended history plus approvalResponses to resume; any pending call without a matching response is denied by default.

Loop guarantees

The loop is self-healing and bounded. A few invariants worth knowing here:

  • Client tools (a Tool with no execute) cannot be auto-run — the loop stops and returns them in toolCalls so the caller owns the round-trip.
  • Runaway guard: the same tool failing on three consecutive steps hard-stops the loop.
  • Stop on tool count, not finishReason: the loop continues whenever the last step emitted tool calls, even if the provider reported finish: stop (a Gemini quirk).

For the full set of agentic invariants — immutable history, parallel execution, the runaway guards, and the Gemini stop-bug guard — see the tool loop reference.

See also

  • streamChat — the streaming counterpart; same orchestration, token-by-token output.
  • generateObject — buffered structured output validated against a schema.
  • Tool loop — the agentic loop's invariants in depth.

Auf dieser Seite