-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathai_tool_loop.ail
More file actions
110 lines (102 loc) · 4 KB
/
Copy pathai_tool_loop.ail
File metadata and controls
110 lines (102 loc) · 4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
-- examples/runnable/ai_tool_loop.ail
-- Demonstrates std/ai.runTools — the multi-turn tool dispatch loop driver
-- introduced by M-AI-TOOL-LOOP (v0.17.0).
--
-- The example builds a 2-tool catalog (read_doc, list_docs), starts a
-- conversation with one user turn ("Summarize the contents of nda.docx"),
-- and lets runTools drive the agent loop:
-- 1. step asks the model what to do
-- 2. model emits a tool_call (e.g. read_doc with name=nda.docx)
-- 3. runTools calls dispatch(call) → "<doc body>"
-- 4. step is called again with the tool result appended
-- 5. model emits a final assistant message → loop terminates
--
-- The dispatch callback in this example is pure — it returns hard-coded
-- strings keyed off the tool name. Real consumers (e.g. motoko_agent)
-- run a callback with effects {FS, Process} that actually reads files /
-- runs commands; AILANG's row polymorphism lets such a callback compose
-- with runTools without changing runTools' signature.
--
-- Setup:
-- ailang run --caps AI,IO --ai gemini-3-flash-preview \
-- examples/runnable/ai_tool_loop.ail
--
-- For deterministic behaviour against the stub handler:
-- ailang run --caps AI,IO --ai-stub examples/runnable/ai_tool_loop.ail
module examples/runnable/ai_tool_loop
import std/ai (
runTools, callResult, AIError, Message, ToolCall, ToolSchema, StepResult
)
import std/io (println)
import std/result (Result, Ok, Err)
import std/list (length)
-- Build a tool catalog the model may call.
export func tools() -> [ToolSchema] {
[
{
name: "read_doc",
description: "Read the named document and return its contents",
parameters: "{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}"
},
{
name: "list_docs",
description: "List the names of all available documents",
parameters: "{\"type\":\"object\",\"properties\":{}}"
}
]
}
-- Initial conversation: one user message.
export func initialMessages() -> [Message] {
[
{
role: "user",
content: "Summarize the contents of nda.docx",
tool_calls: [],
tool_call_id: ""
}
]
}
-- Dispatch callback. In a real agent this would do file I/O / shell exec /
-- HTTP requests / etc. with appropriate effects. Here we return canned
-- strings so the example is deterministic and runs offline against any
-- AI handler that emits tool_calls.
export pure func dispatch(call: ToolCall) -> string {
if call.name == "read_doc"
then "[NDA between Acme Corp and Beta LLC, dated 2026-01-15. Confidential information defined as ... 2-year term ... governed by Delaware law.]"
else if call.name == "list_docs"
then "nda.docx, employment_contract.docx, lease.pdf"
else "unknown tool: ${call.name}"
}
-- Render the typed-error path with retry-decision branch.
export pure func renderError(e: AIError) -> string {
let retryStr = if e.retryable then "true" else "false";
"[${e.code}] retryable=${retryStr}: ${e.message}"
}
-- Pull the last message's content for display. If somehow empty (shouldn't
-- happen — runTools always appends at least one message on Ok), return a
-- sentinel so the example output is still informative.
export pure func lastMessage(messages: [Message]) -> string {
match messages {
[] => "<no messages>",
[msg] => msg.content,
[_, ...rest] => lastMessage(rest)
}
}
export func main() -> () ! {AI, IO} {
-- Cap the loop at 8 steps. Reasonable for read_doc → summarize flows;
-- raise for deeper agent traces.
match runTools("", initialMessages(), tools(), dispatch, 8) {
Ok(messages) => {
let n = length(messages);
println("=== runTools succeeded: ${show(n)} messages in transcript ===");
println("Final assistant text:");
println(lastMessage(messages))
},
Err(e) =>
-- Typed-error retry-decision branch — the whole point of returning
-- AIError instead of a plain string.
if e.retryable
then println("Transient error (would retry): ${renderError(e)}")
else println("Fatal error: ${renderError(e)}")
}
}