Tool loop in TypeScript
The same library assistant as Tool loop in Python, typed, with the openai package pointed at Syntic. Run it with npm install openai tsx and npx tsx loop.ts.
// loop.ts
import OpenAI from 'openai'
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions'
const client = new OpenAI({ baseURL: 'https://app.syntic.ai/v1', apiKey: process.env.SYNTIC_API_KEY })
// --- Your functions ---------------------------------------------------------
type Book = { title: string; copies: number; onLoan: number; dueBack: string | null }
const LIBRARY: Record<string, Book> = {
'978-0141439518': { title: 'Pride and Prejudice', copies: 3, onLoan: 3, dueBack: '2026-10-19' },
'978-0007119318': { title: 'The Hobbit', copies: 2, onLoan: 1, dueBack: null },
}
const functions: Record<string, (args: any) => unknown> = {
find_book: ({ query }: { query: string }) => ({
results: Object.entries(LIBRARY)
.filter(([, b]) => b.title.toLowerCase().includes(String(query).toLowerCase()))
.map(([isbn, b]) => ({ isbn, title: b.title })),
}),
availability: ({ isbn }: { isbn: string }) => {
const b = LIBRARY[isbn]
if (!b) return { error: `no book with ISBN ${isbn}` }
const free = b.copies - b.onLoan
return { available: free, next_due_back: free ? null : b.dueBack }
},
}
// --- How they are described to the model ----------------------------------
const tools: ChatCompletionTool[] = [
{
type: 'function',
function: {
name: 'find_book',
description: 'Search the catalogue by title words. Returns ISBNs.',
parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
},
},
{
type: 'function',
function: {
name: 'availability',
description: 'Copies available now for an ISBN, and when the next one is due back.',
parameters: { type: 'object', properties: { isbn: { type: 'string' } }, required: ['isbn'] },
},
},
]
// --- The loop ----------------------------------------------------------------
export async function run(question: string, maxRounds = 8): Promise<string> {
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: `You help members of a town library. Today is ${new Date().toISOString().slice(0, 10)}. Use the tools; never guess availability.` },
{ role: 'user', content: question },
]
for (let round = 0; round < maxRounds; round++) {
const reply = await client.chat.completions.create({
model: 'Amara S1.2', stream: false, max_tokens: 800, tools, messages,
})
const msg = reply.choices[0].message
if (!msg.tool_calls?.length) return msg.content ?? ''
messages.push(msg) // the assistant turn, unchanged
for (const call of msg.tool_calls) {
let result: unknown
try {
const fn = functions[call.function.name]
if (!fn) throw new Error(`unknown tool ${call.function.name}`)
result = fn(JSON.parse(call.function.arguments || '{}'))
} catch (err) {
result = { error: (err as Error).message }
}
messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) })
}
}
return "Sorry, I couldn't finish that request."
}
console.log(await run('Can I borrow Pride and Prejudice today? If not, when?'))Notes
call.function.argumentsis a JSON string. Parse it insidetry, because it can be invalid.- Treat
call.idas an opaque string; its format is not fixed. - Pass the same
toolsarray on every round so the prompt prefix stays identical and cacheable. - Depending on your
openaipackage version, the tool-call type may be a union that includes non-function calls; narrow it withcall.type === 'function'if the compiler asks.
Related
- Tool loop in Python
- Build an agent: budgets, approvals and traces around this loop.
- Tool calling
Was this page helpful?