Chat
An agent is one definition. Nothing forces it to do both text and voice - a text agent needs no voice, no transcriber and no phone number, and a voice agent needs no chat widget. The prompt, the tools, the knowledge base and variables behave identically either way.
| Text | Voice | |
|---|---|---|
| What you build with | threads, or the browser chat widget | calls, phoneNumbers, campaigns |
| How it is metered | per token, plus a flat fee per message | per token, per character and per minute by component, plus a flat platform fee per minute |
| Settings that apply | model, prompt, tools, knowledge, variables | those, plus voice, transcriber and turn-taking |
The same agent can do both if you want it to. See Pricing for how each is metered.
Threads and runs
A thread holds one conversation. A run is one turn on it: you add a user message, the agent replies, and the thread keeps the history so you never resend it.
A run completes before the call returns, so there is nothing to poll - the reply is already in the result. The thread carries the agent id it was opened against, so no later call has to repeat it.
import { Glytos } from '@glytos/node';
const glytos = new Glytos(process.env.GLYTOS_API_KEY!);
const thread = await glytos.threads.create({ agent: agentUuid });
const run = await glytos.threads.runs.create(thread, 'What are your opening hours?');
console.log(run.messages.at(-1)?.content);from glytos import Glytos
glytos = Glytos(api_key=os.environ["GLYTOS_API_KEY"])
thread = glytos.threads.create(agent=agent_uuid)
run = glytos.threads.runs.create(thread, "What are your opening hours?")
print(run["messages"][-1]["content"])$glytos = new Glytos\Client(apiKey: getenv('GLYTOS_API_KEY'));
$thread = $glytos->threads->create($agentUuid);
$run = $glytos->threads->runs->create($thread, 'What are your opening hours?');
echo end($run['messages'])['content'];using var glytos = new GlytosClient(apiKey);
var thread = await glytos.Threads.CreateAsync(agentUuid);
var run = await glytos.Threads.Runs.CreateAsync(thread, "What are your opening hours?");
Console.WriteLine(run.GetProperty("messages")[0].GetProperty("content").GetString());client := glytos.New(os.Getenv("GLYTOS_API_KEY"))
thread, _ := client.Threads.Create(ctx, agentUUID, nil)
run, _ := client.Threads.Runs.Create(ctx, *thread, &glytos.TurnParams{
Content: "What are your opening hours?",
})# Open a thread
curl -X POST https://api.glytos.com/api/v1/workflows/$AGENT/sessions \
-H "X-API-Key: $GLYTOS_API_KEY" -H 'Content-Type: application/json' -d '{}'
# Run a turn on it
curl -X POST https://api.glytos.com/api/v1/workflows/$AGENT/sessions/$SESSION/messages \
-H "X-API-Key: $GLYTOS_API_KEY" -H 'Content-Type: application/json' \
-d '{"content":"What are your opening hours?"}'A run returns only that turn's messages. The whole conversation is on the thread:
const history = await glytos.threads.messages.list(thread);Opening a thread with variables seeds it, and those values are available to the
prompt for the rest of the conversation - see Variables.
Streaming
A long answer should not arrive as one silent wait. Streaming yields the reply as it
is written, then a terminal done event carrying the same payload the non-streamed
call returns. Routing, persistence and billing are identical either way.
for await (const event of glytos.threads.runs.stream(thread, 'Summarise the policy')) {
if (event.type === 'token') process.stdout.write(event.delta);
if (event.type === 'done') console.log(event.run.status);
}for event in glytos.threads.runs.stream(thread, "Summarise the policy"):
if event.type == "token":
print(event.delta, end="", flush=True)foreach ($glytos->threads->runs->stream($thread, 'Summarise the policy') as $event) {
if ($event->type === 'token') {
echo $event->delta;
}
}await foreach (var e in glytos.Threads.Runs.StreamAsync(thread, "Summarise the policy"))
{
if (e.Type == "token") Console.Write(e.Delta);
}err := client.Threads.Runs.Stream(ctx, *thread,
&glytos.TurnParams{Content: "Summarise the policy"},
func(e glytos.StreamEvent) error {
if e.Type == "token" {
fmt.Print(e.Delta)
}
return nil
})glytos chat $AGENT -m "Summarise the policy"The stream is Server-Sent Events. Calling the API directly, each frame is
event: token, done or error followed by a data: JSON line, with a blank line
between frames.
Per-turn instructions
Extra context that applies to one run only. It sits below the agent's own instructions and is never saved to the agent, so a scoring pass or a one-off format demand does not permanently change how the agent behaves.
await glytos.threads.runs.create(thread, {
content: 'Rate this transcript',
instructions: 'Score 1-5 and reply as JSON.',
});glytos.threads.runs.create(
thread,
"Rate this transcript",
instructions="Score 1-5 and reply as JSON.",
)$glytos->threads->runs->create($thread, 'Rate this transcript', null, 'Score 1-5, reply as JSON.');await glytos.Threads.Runs.CreateAsync(
thread,
"Rate this transcript",
instructions: "Score 1-5 and reply as JSON.");curl -X POST https://api.glytos.com/api/v1/workflows/$AGENT/sessions/$SESSION/messages \
-H "X-API-Key: $GLYTOS_API_KEY" -H 'Content-Type: application/json' \
-d '{"content":"Rate this","additional_instructions":"Score 1-5 and reply as JSON."}'The message may be empty when the instructions carry the turn: re-running a conversation with new instructions and no new message is a legitimate run.
Images and attachments
Images ride on a single turn, and a text-only model simply ignores them:
await glytos.threads.runs.create(thread, {
content: 'What is on this label?',
images: [dataUri],
});A file is attached to one conversation. Its text is put in front of the agent for that conversation only - it does not join the knowledge base and it does not reach any other thread:
await glytos.chat.uploadFile({
token,
sessionUuid: thread.id,
file: buffer,
filename: 'policy.txt',
});For documents every conversation should be able to reach, use the knowledge base instead.
In the browser
For a chat widget on your own site, use @glytos/web with a short-lived chat
token you mint on your backend - never an API key.
import { GlytosChat } from '@glytos/web';
const chat = new GlytosChat({ token });
chat.on('token', t => append(t.delta));
chat.on('message', m => render(m));
await chat.start();
await chat.stream('Hello!');The conversation id is on chat.sessionId; persist it and pass it back as
sessionUuid to pick the same conversation up later.
Per-turn instructions are deliberately not available in the browser: the widget runs on a public page, so such a field would let anyone rewrite your agent's behaviour on your own site. Use a server SDK for that.
See the Web SDK.
Organising agents
Folders group agents inside one environment, so Development and Production organise independently:
const folder = await glytos.folders.create('Support');
await glytos.agents.moveToFolder(agentUuid, folder.uuid);Deleting a folder deletes the agents filed in it, so the confirmation states the count. Their financial records survive; their own call history does not.
What it costs
Text is metered per token at the model's own rate, plus a small flat fee per message. A canned message step that runs no model is not billed. Bring your own provider key and the token cost is yours - only the flat fee applies. See Pricing.
Where to look next
- Tools - text runs the same saved tools voice does.
- Sessions - every thread is a session, with its full transcript, cost breakdown and event log.
- API reference - every endpoint.