Skip to main content

Ai.Text

Examples below assume this module is imported with an imports: entry under alias Ai (and ai-openai as AiOpenai). Kind references (Ai.Text, AiOpenai.OpenaiModel, …) follow those aliases — if you import either module under a different name, substitute accordingly.

Ai.Text is a Telo.Invocable that delegates a single-turn, buffered LLM call to any Ai.Model implementation. It owns message-building, system-prompt handling, and option-merging; the model handles the HTTP call. For chunked output, see Ai.TextStream.

kind: Telo.Application
metadata: { name: summarizer, version: 1.0.0 }
imports:
Ai: std/ai@0.7.0
AiOpenai: std/ai-openai@0.8.0
---
kind: AiOpenai.OpenaiModel
metadata: { name: Gpt4o }
model: gpt-4o
apiKey: "${{ secrets.OPENAI_API_KEY }}"
---
kind: Ai.Text
metadata: { name: Summarizer }
model: !ref Gpt4o
system: "Summarize in one sentence."
options:
temperature: 0.2

Manifest fields

FieldTypeRequiredPurpose
modelrefyesReference to any Ai.Model implementation. Typed x-telo-ref: "std/ai#Model".
systemstringnoDefault system prompt. Runtime inputs.system wins when set.
optionsobjectnoResource-level option defaults. Merged beneath inputs.options (downstream wins).

The model field uses identity-form x-telo-ref because the schema is part of @telorun/ai's public surface — it must resolve regardless of who imports it. (extends, by contrast, uses alias-form because it's evaluated in the declaring file's own import scope. See kernel/docs/inheritance.md.)

Invocation inputs

FieldTypeRequiredPurpose
promptstringexactly one of prompt/messagesShorthand; wraps to messages: [{role: "user", content: prompt}].
messagesarrayexactly one of prompt/messagesFull turns, each {role, content}.
systemstringnoRuntime system override. Wins over manifest system.
optionsobjectnoPer-call option overrides.

Validation: passing both prompt and messages, or neither, throws InvokeError("ERR_INVALID_INPUT", …). Each message is checked for role ∈ {system, user, assistant} and a string content; off-contract values throw the same code.

Output

{
text: string;
usage: { promptTokens: number; completionTokens: number; totalTokens: number };
finishReason: "stop" | "length" | "content-filter" | "error" | "other";
}

The controller validates the model's return value before forwarding; if a provider deviates, it throws InvokeError("ERR_CONTRACT_VIOLATION", …).

Option layering

Four conceptual layers, three of them user-visible. Shallow merge; downstream wins.

#SourceWhen merged
0Provider hard defaults (controller)Inside the provider, before vendor call.
1Ai.<Provider>Model.options (manifest)Inside the provider, on top of layer 0.
2Ai.Text.options (manifest)Inside the Ai.Text controller, before delegating.
3inputs.options at invocation timeInside the Ai.Text controller, on top of layer 2.

The provider receives layers 2+3 as the options bag and merges layers 0+1 internally.

System-prompt rules

runtime inputs.system  >  manifest system  >  inline messages[0] when role: system

If the messages array already starts with role: system, a runtime/manifest system replaces that message's content. Otherwise the system message is prepended. Either way, exactly one system message ends up in the canonical messages array.

Run.Sequence integration

Ai.Text is a regular Invocable, so it slots straight into Run.Sequence:

kind: Run.Sequence
metadata: { name: SummarizeArticle }
steps:
- name: Summarize
inputs:
prompt: "Summarize:\n${{ vars.articleText }}"
invoke: !ref Summarizer
- name: Save
inputs:
summary: "${{ steps.Summarize.result.text }}"
invoke:
kind: Sql.Exec
connection: !ref Db
inputs:
sql: "INSERT INTO summaries (text) VALUES (?)"
bindings: ["${{ inputs.summary }}"]

steps.Summarize.result.{text,usage,finishReason} is fully typed — the analyzer's x-telo-step-context derives it from Ai.Text's own declared outputType.

What's NOT here

  • Streaming. Ai.Text is buffered; chunked output lives in Ai.TextStream, which shares the same provider resources via Ai.Model.
  • Tool use / function calling. Lives in the future Ai.Agent / Ai.Worker kinds.
  • Multimodal input. content is a string today. Widening to string | ContentPart[] later is non-breaking.