Artifacts (Canvas)
Artifacts (also known as a “Canvas”) give the agent a persistent work surface separate from the chat. Use artifacts to hold substantive deliverables (documents, specs, code), while keeping chat replies brief and status‑oriented.
You can also check out the Console App to see how artifacts are integrated into a chat interface.
Implementation
Section titled “Implementation”The examples below implement a simple artifacts workflow as one toolkit. Enabling the toolkit provides these operations together:
artifact_create: create a new documentartifact_update: replace content and print a color diffartifact_get/artifact_list: fetch and list documentsartifact_delete: remove a document

import { Agent, type AgentItem, type AgentTool, type Toolkit, type ToolkitSession,} from "@hoangvvo/llm-agent";import { zodTool } from "@hoangvvo/llm-agent/zod";import { diffLines } from "diff";import { z } from "zod";import { getModel } from "./get-model.ts";
// Artifacts/Canvas feature example: the agent maintains named deliverables// (documents/code/specs) separate from the chat through one toolkit that// provides create, update, retrieve, list, and delete operations together.
type ArtifactKind = "markdown" | "text" | "code";
interface Artifact { id: string; title: string; kind: ArtifactKind; content: string; version: number; updated_at: string;}
class InMemoryArtifactStore { #map = new Map<string, Artifact>();
create(a: { id?: string; title: string; kind: ArtifactKind; content: string; }): Artifact { const id = (a.id?.trim() ? a.id : Math.random().toString(36).slice(2, 11))!; const now = new Date().toISOString(); const artifact: Artifact = { id, title: a.title, kind: a.kind, content: a.content, version: 1, updated_at: now, }; this.#map.set(id, artifact); return artifact; }
update(a: { id: string; content: string }): Artifact { const existing = this.#map.get(a.id); if (!existing) throw new Error(`Artifact not found: ${a.id}`); const next: Artifact = { ...existing, content: a.content, version: existing.version + 1, updated_at: new Date().toISOString(), }; this.#map.set(a.id, next); return next; }
get(id: string): Artifact { const a = this.#map.get(id); if (!a) throw new Error(`Artifact not found: ${id}`); return a; }
list(): Artifact[] { return [...this.#map.values()].sort((a, b) => a.id.localeCompare(b.id)); }
delete(id: string): { success: boolean } { const existed = this.#map.delete(id); return { success: existed }; }}
const store = new InMemoryArtifactStore();const provider = process.env["PROVIDER"] ?? "openai";const modelId = process.env["MODEL"] ?? "gpt-5.6-terra";const model = getModel(provider, modelId);
const overviewPrompt = `Use artifacts (documents/canvases) for substantive deliverables like documents, plans, specs, or code. Keep chat replies brief and status-oriented; put the full content into an artifact via the tools. Always reference artifacts by id.`;
const rulesPrompt = `- Prefer creating/updating artifacts instead of pasting large content into chat- When asked to revise or extend prior work, read/update the relevant artifact- Keep the chat response short: what changed, where it lives (artifact id), and next steps`;
// Minimal colored diff rendering (single dep: diff)const color = (s: string, code: number) => `\x1b[${code}m${s}\x1b[0m`;const green = (s: string) => color(s, 32);const red = (s: string) => color(s, 31);const dim = (s: string) => color(s, 2);function renderDiff(oldText: string, newText: string): string { const parts = diffLines(oldText, newText); const lines: string[] = []; for (const p of parts) { const valLines = p.value.replace(/\n$/, "").split("\n"); for (const ln of valLines) { if (p.added) lines.push(green(`+ ${ln}`)); else if (p.removed) lines.push(red(`- ${ln}`)); else lines.push(dim(` ${ln}`)); } } return lines.join("\n");}
const artifactTools: AgentTool<void>[] = [ zodTool({ name: "artifact_create", description: "Create a new artifact (document/canvas) and return the created artifact", parameters: z.object({ title: z.string(), kind: z.enum(["markdown", "text", "code"]), content: z.string(), }), async execute(args) { console.log( `[artifacts.create] id=(auto) title=${args.title} kind=${args.kind}`, ); const artifact = store.create(args); return { content: [{ type: "text", text: JSON.stringify({ artifact }) }], is_error: false, }; }, }), zodTool({ name: "artifact_update", description: "Replace the content of an existing artifact and return it", parameters: z.object({ id: z.string(), content: z.string() }), async execute({ id, content }) { const before = store.get(id).content; console.log(`[artifacts.update] id=${id} len=${content.length}`); const artifact = store.update({ id, content }); console.log( "\n=== Diff (old → new) ===\n" + renderDiff(before, artifact.content) + "\n========================\n", ); return { content: [{ type: "text", text: JSON.stringify({ artifact }) }], is_error: false, }; }, }), zodTool({ name: "artifact_get", description: "Fetch a single artifact by id", parameters: z.object({ id: z.string() }), async execute({ id }) { console.log(`[artifacts.get] id=${id}`); const artifact = store.get(id); return { content: [{ type: "text", text: JSON.stringify({ artifact }) }], is_error: false, }; }, }), zodTool({ name: "artifact_list", description: "List all artifacts", parameters: z.object({}), async execute() { console.log(`[artifacts.list]`); const artifacts = store.list(); return { content: [{ type: "text", text: JSON.stringify({ artifacts }) }], is_error: false, }; }, }), zodTool({ name: "artifact_delete", description: "Delete an artifact by id", parameters: z.object({ id: z.string() }), async execute({ id }) { console.log(`[artifacts.delete] id=${id}`); const result = store.delete(id); return { content: [{ type: "text", text: JSON.stringify(result) }], is_error: false, }; }, }),];
class ArtifactsToolkitSession implements ToolkitSession<void> { getSystemPrompt(): string { return `${overviewPrompt}\n${rulesPrompt}`; }
getTools(): AgentTool<void>[] { return artifactTools; }
async close(): Promise<void> { // No resources to release. }}
class ArtifactsToolkit implements Toolkit<void> { async createSession(): Promise<ToolkitSession<void>> { return new ArtifactsToolkitSession(); }}
const artifactsAgent = new Agent<void>({ name: "artifacts", model, toolkits: [new ArtifactsToolkit()],});
// Demo: ask the agent to create an artifact, then revise it.const items1: AgentItem[] = [ { type: "message", role: "user", content: [ { type: "text", text: `We need a product requirements document for a new Todo app.Please draft it in markdown with sections: Overview, Goals, Non-Goals, Requirements.Keep your chat reply short and save the full document to a separate document we can keep iterating on.`, }, ], },];
const res1 = await artifactsAgent.run({ context: undefined, input: items1 });console.dir(res1.content, { depth: null });console.log("Artifacts after creation:");console.dir(store.list(), { depth: null });
const items2: AgentItem[] = [ { type: "message", role: "user", content: [ { type: "text", text: `Please revise the document: expand the Goals section with 3 concrete goals and add a Milestones section. Keep your chat reply brief.`, }, ], },];
const res2 = await artifactsAgent.run({ context: undefined, input: items2 });console.dir(res2.content, { depth: null });console.log("Artifacts after update:");console.dir(store.list(), { depth: null });use dotenvy::dotenv;use futures::future::BoxFuture;use llm_agent::{ Agent, AgentFunctionTool, AgentItem, AgentRequest, AgentTool, AgentToolResult, BoxedError, Toolkit, ToolkitSession,};use llm_sdk::{JSONSchema, Message, Part};use serde::{Deserialize, Serialize};use serde_json::json;use std::{ collections::HashMap, sync::{Arc, Mutex},};
mod common;
#[derive(Clone, Serialize, Debug)]struct Artifact { id: String, title: String, kind: String, content: String, version: u32, updated_at: String,}
#[derive(Default, Clone)]struct Store { m: Arc<Mutex<HashMap<String, Artifact>>>,}
impl Store { fn create(&self, title: String, kind: String, content: String) -> Artifact { let id = format!( "{:x}", chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) ); let a = Artifact { id: id.clone(), title, kind, content, version: 1, updated_at: chrono::Utc::now().to_rfc3339(), }; self.m.lock().unwrap().insert(id.clone(), a.clone()); a } fn update(&self, id: &str, content: String) -> (Artifact, String) { let mut map = self.m.lock().unwrap(); let a = map.get_mut(id).expect("artifact not found"); let before = a.content.clone(); a.content = content; a.version += 1; a.updated_at = chrono::Utc::now().to_rfc3339(); (a.clone(), before) } fn get(&self, id: &str) -> Artifact { self.m .lock() .unwrap() .get(id) .expect("artifact not found") .clone() } fn list(&self) -> Vec<Artifact> { self.m.lock().unwrap().values().cloned().collect() } fn delete(&self, id: &str) -> bool { self.m.lock().unwrap().remove(id).is_some() }}
// Minimal colored line diff using similarfn render_diff(old_text: &str, new_text: &str) -> String { use similar::{ChangeTag, TextDiff}; let diff = TextDiff::from_lines(old_text, new_text); let mut out = String::new(); for change in diff.iter_all_changes() { let (sign, color) = match change.tag() { ChangeTag::Delete => ("- ", "\x1b[31m"), ChangeTag::Insert => ("+ ", "\x1b[32m"), ChangeTag::Equal => (" ", "\x1b[2m"), }; out.push_str(color); out.push_str(sign); out.push_str(change.to_string().as_str()); out.push_str("\x1b[0m"); } out}
// No contexttype Ctx = ();
struct ArtifactCreate { store: Store,}impl AgentFunctionTool<Ctx> for ArtifactCreate { fn name(&self) -> String { "artifact_create".into() } fn description(&self) -> String { "Create a new document and return it".into() } fn parameters(&self) -> JSONSchema { json!({ "type":"object", "properties":{ "title":{"type":"string"}, "kind":{"type":"string", "enum":["markdown","text","code"]}, "content":{"type":"string"} }, "required":["title","kind","content"], "additionalProperties":false }) } fn execute<'a>( &'a self, args: serde_json::Value, _ctx: &'a Ctx, _state: &llm_agent::RunState, ) -> BoxFuture<'a, Result<AgentToolResult, Box<dyn std::error::Error + Send + Sync>>> { Box::pin(async move { #[derive(Deserialize)] struct In { title: String, kind: String, content: String, } let p: In = serde_json::from_value(args)?; println!("[artifacts.create] title={} kind={}", p.title, p.kind); let a = self.store.create(p.title, p.kind, p.content); Ok(AgentToolResult { content: vec![Part::text(json!({"artifact":a}).to_string())], is_error: false, }) }) }}
struct ArtifactUpdate { store: Store,}impl AgentFunctionTool<Ctx> for ArtifactUpdate { fn name(&self) -> String { "artifact_update".into() } fn description(&self) -> String { "Replace the content of a document and return it".into() } fn parameters(&self) -> JSONSchema { json!({ "type":"object", "properties":{ "id":{"type":"string"}, "content":{"type":"string"} }, "required":["id","content"], "additionalProperties":false }) } fn execute<'a>( &'a self, args: serde_json::Value, _ctx: &'a Ctx, _state: &llm_agent::RunState, ) -> BoxFuture<'a, Result<AgentToolResult, Box<dyn std::error::Error + Send + Sync>>> { Box::pin(async move { #[derive(Deserialize)] struct In { id: String, content: String, } let p: In = serde_json::from_value(args)?; let before = self.store.get(&p.id).content; println!("[artifacts.update] id={} len={}", p.id, p.content.len()); let (a, _before) = self.store.update(&p.id, p.content); println!( "\n=== Diff (old → new) ===\n{}========================\n", render_diff(&before, &a.content) ); Ok(AgentToolResult { content: vec![Part::text(json!({"artifact":a}).to_string())], is_error: false, }) }) }}
struct ArtifactGet { store: Store,}impl AgentFunctionTool<Ctx> for ArtifactGet { fn name(&self) -> String { "artifact_get".into() } fn description(&self) -> String { "Fetch a document by id".into() } fn parameters(&self) -> JSONSchema { json!({"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}) } fn execute<'a>( &'a self, args: serde_json::Value, _ctx: &'a Ctx, _state: &llm_agent::RunState, ) -> BoxFuture<'a, Result<AgentToolResult, Box<dyn std::error::Error + Send + Sync>>> { Box::pin(async move { #[derive(Deserialize)] struct In { id: String, } let p: In = serde_json::from_value(args)?; println!("[artifacts.get] id={}", p.id); let a = self.store.get(&p.id); Ok(AgentToolResult { content: vec![Part::text(json!({"artifact":a}).to_string())], is_error: false, }) }) }}
struct ArtifactList { store: Store,}impl AgentFunctionTool<Ctx> for ArtifactList { fn name(&self) -> String { "artifact_list".into() } fn description(&self) -> String { "List all documents".into() } fn parameters(&self) -> JSONSchema { json!({"type":"object","properties":{},"additionalProperties":false}) } fn execute<'a>( &'a self, _args: serde_json::Value, _ctx: &'a Ctx, _state: &'a llm_agent::RunState, ) -> BoxFuture<'a, Result<AgentToolResult, Box<dyn std::error::Error + Send + Sync>>> { Box::pin(async move { println!("[artifacts.list]"); let list = self.store.list(); Ok(AgentToolResult { content: vec![Part::text(json!({"artifacts":list}).to_string())], is_error: false, }) }) }}
struct ArtifactDelete { store: Store,}impl AgentFunctionTool<Ctx> for ArtifactDelete { fn name(&self) -> String { "artifact_delete".into() } fn description(&self) -> String { "Delete a document by id".into() } fn parameters(&self) -> JSONSchema { json!({"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}) } fn execute<'a>( &'a self, args: serde_json::Value, _ctx: &'a Ctx, _state: &'a llm_agent::RunState, ) -> BoxFuture<'a, Result<AgentToolResult, Box<dyn std::error::Error + Send + Sync>>> { Box::pin(async move { #[derive(Deserialize)] struct In { id: String, } let p: In = serde_json::from_value(args)?; println!("[artifacts.delete] id={}", p.id); let ok = self.store.delete(&p.id); Ok(AgentToolResult { content: vec![Part::text(json!({"success":ok}).to_string())], is_error: false, }) }) }}
const ARTIFACTS_PROMPT: &str = "Use documents (artifacts/canvases) for substantive deliverables \like documents, plans, specs, or code. Keep chat replies brief and status-oriented; put the full \content into a document via the tools. Always reference documents by id.\n- Prefer \creating/updating documents instead of pasting large content into chat\n- When asked to revise or \extend prior work, read/update the relevant document\n- Keep the chat response short: what \changed, where it lives (document id), and next steps";
struct ArtifactsToolkit { store: Store,}
impl Toolkit<Ctx> for ArtifactsToolkit { fn create_session<'a>( &'a self, _context: &'a Ctx, ) -> BoxFuture<'a, Result<Box<dyn ToolkitSession<Ctx> + Send + Sync>, BoxedError>> { Box::pin(async move { let session: Box<dyn ToolkitSession<Ctx> + Send + Sync> = Box::new(ArtifactsToolkitSession { store: self.store.clone(), }); Ok(session) }) }}
struct ArtifactsToolkitSession { store: Store,}
impl ToolkitSession<Ctx> for ArtifactsToolkitSession { fn system_prompt(&self) -> Option<String> { Some(ARTIFACTS_PROMPT.to_string()) }
fn tools(&self) -> Vec<AgentTool<Ctx>> { vec![ AgentTool::function(ArtifactCreate { store: self.store.clone(), }), AgentTool::function(ArtifactUpdate { store: self.store.clone(), }), AgentTool::function(ArtifactGet { store: self.store.clone(), }), AgentTool::function(ArtifactList { store: self.store.clone(), }), AgentTool::function(ArtifactDelete { store: self.store.clone(), }), ] }
fn close(self: Box<Self>) -> BoxFuture<'static, Result<(), BoxedError>> { Box::pin(async move { Ok(()) }) }}
#[tokio::main]async fn main() { dotenv().ok(); let provider = std::env::var("PROVIDER").unwrap_or_else(|_| "openai".to_string()); let model_id = std::env::var("MODEL").unwrap_or_else(|_| "gpt-5.6-terra".to_string()); let model = common::get_model( &provider, &model_id, llm_sdk::LanguageModelMetadata::default(), None, ) .expect("failed to create model");
let store = Store::default(); let agent = Agent::new(llm_agent::AgentParams::new("artifacts", model).add_toolkit( ArtifactsToolkit { store: store.clone(), }, ));
let items1: Vec<AgentItem> = vec![AgentItem::Message(Message::user(vec![Part::text( "We need a product requirements document for a new Todo app. Please draft it in markdown \ with sections: Overview, Goals, Non-Goals, Requirements. Keep your chat reply short and \ save the full document to a separate document we can keep iterating on.", )]))]; let res1 = agent .run(AgentRequest { context: (), input: items1, }) .await .expect("run failed"); println!("{:?}", res1.content); println!("Documents after creation:\n{:?}", store.list());
let items2: Vec<AgentItem> = vec![AgentItem::Message(Message::user(vec![Part::text( "Please revise the document: expand the Goals section with 3 concrete goals and add a \ Milestones section. Keep your chat reply brief.", )]))]; let res2 = agent .run(AgentRequest { context: (), input: items2, }) .await .expect("run failed"); println!("{:?}", res2.content); println!("Documents after update:\n{:?}", store.list());}package main
import ( "context" "encoding/json" "fmt" "log" "os" "strings" "time"
dmp "github.com/sergi/go-diff/diffmatchpatch"
llmagent "github.com/hoangvvo/llm-sdk/agent-go" "github.com/hoangvvo/llm-sdk/agent-go/examples" llmsdk "github.com/hoangvvo/llm-sdk/sdk-go" "github.com/joho/godotenv" "github.com/sanity-io/litter")
// Artifacts/Canvas feature in Go: maintain named documents separate from chat.
type ArtifactKind string
const ( KindMarkdown ArtifactKind = "markdown" KindText ArtifactKind = "text" KindCode ArtifactKind = "code")
type Artifact struct { ID string `json:"id"` Title string `json:"title"` Kind ArtifactKind `json:"kind"` Content string `json:"content"` Version int `json:"version"` UpdatedAt string `json:"updated_at"`}
type Store struct{ m map[string]*Artifact }
func NewStore() *Store { return &Store{m: map[string]*Artifact{}} }
func (s *Store) Create(title string, kind ArtifactKind, content string) *Artifact { id := randID() a := &Artifact{ID: id, Title: title, Kind: kind, Content: content, Version: 1, UpdatedAt: time.Now().UTC().Format(time.RFC3339)} s.m[id] = a return a}
func (s *Store) Update(id, content string) (*Artifact, string) { a, ok := s.m[id] if !ok { panic("artifact not found: " + id) } before := a.Content a.Content = content a.Version++ a.UpdatedAt = time.Now().UTC().Format(time.RFC3339) return a, before}
func (s *Store) Get(id string) *Artifact { a, ok := s.m[id] if !ok { panic("artifact not found: " + id) } return a}
func (s *Store) List() []*Artifact { out := make([]*Artifact, 0, len(s.m)) for _, a := range s.m { out = append(out, a) } return out}
func (s *Store) Delete(id string) bool { _, ok := s.m[id]; delete(s.m, id); return ok }
// Minimal colored line diff using go-difffunc renderDiff(oldText, newText string) string { d := dmp.New() diffs := d.DiffMain(oldText, newText, false) // Coalesce tiny changes to lines d.DiffCleanupSemantic(diffs) var b strings.Builder lines := func(s string) []string { return strings.Split(strings.TrimSuffix(s, "\n"), "\n") } for _, df := range diffs { switch df.Type { case dmp.DiffInsert: for _, ln := range lines(df.Text) { b.WriteString("\x1b[32m+ " + ln + "\x1b[0m\n") } case dmp.DiffDelete: for _, ln := range lines(df.Text) { b.WriteString("\x1b[31m- " + ln + "\x1b[0m\n") } default: for _, ln := range lines(df.Text) { b.WriteString("\x1b[2m " + ln + "\x1b[0m\n") } } } return b.String()}
// No contexttype Ctx = struct{}
// Toolstype CreateParams struct { Title string `json:"title"` Kind string `json:"kind"` Content string `json:"content"`}type UpdateParams struct { ID string `json:"id"` Content string `json:"content"`}type GetParams struct { ID string `json:"id"`}type DeleteParams struct { ID string `json:"id"`}
type ArtifactCreateTool struct{ S *Store }
func (t *ArtifactCreateTool) Name() string { return "artifact_create" }func (t *ArtifactCreateTool) Description() string { return "Create a new document and return it" }func (t *ArtifactCreateTool) Parameters() llmsdk.JSONSchema { m := llmsdk.JSONSchema{} m["type"] = "object" props := map[string]any{ "title": map[string]any{"type": "string"}, "kind": map[string]any{"type": "string", "enum": []string{"markdown", "text", "code"}}, "content": map[string]any{"type": "string"}, } m["properties"] = props m["required"] = []string{"title", "kind", "content"} m["additionalProperties"] = false return m}func (t *ArtifactCreateTool) Execute(_ context.Context, params json.RawMessage, _ Ctx, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var p CreateParams if err := json.Unmarshal(params, &p); err != nil { return llmagent.AgentToolResult{}, err } fmt.Printf("[artifacts.create] title=%s kind=%s\n", p.Title, p.Kind) a := t.S.Create(p.Title, ArtifactKind(p.Kind), p.Content) body, _ := json.Marshal(map[string]interface{}{"artifact": a}) return llmagent.AgentToolResult{Content: []llmsdk.Part{llmsdk.NewTextPart(string(body))}, IsError: false}, nil}
type ArtifactUpdateTool struct{ S *Store }
func (t *ArtifactUpdateTool) Name() string { return "artifact_update" }func (t *ArtifactUpdateTool) Description() string { return "Replace the content of a document and return it"}func (t *ArtifactUpdateTool) Parameters() llmsdk.JSONSchema { m := llmsdk.JSONSchema{} m["type"] = "object" m["properties"] = map[string]any{"id": map[string]any{"type": "string"}, "content": map[string]any{"type": "string"}} m["required"] = []string{"id", "content"} m["additionalProperties"] = false return m}func (t *ArtifactUpdateTool) Execute(_ context.Context, params json.RawMessage, _ Ctx, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var p UpdateParams if err := json.Unmarshal(params, &p); err != nil { return llmagent.AgentToolResult{}, err } before := t.S.Get(p.ID).Content fmt.Printf("[artifacts.update] id=%s len=%d\n", p.ID, len(p.Content)) a, _ := t.S.Update(p.ID, p.Content) fmt.Printf("\n=== Diff (old → new) ===\n%s========================\n\n", renderDiff(before, a.Content)) body, _ := json.Marshal(map[string]interface{}{"artifact": a}) return llmagent.AgentToolResult{Content: []llmsdk.Part{llmsdk.NewTextPart(string(body))}, IsError: false}, nil}
type ArtifactGetTool struct{ S *Store }
func (t *ArtifactGetTool) Name() string { return "artifact_get" }func (t *ArtifactGetTool) Description() string { return "Fetch a document by id" }func (t *ArtifactGetTool) Parameters() llmsdk.JSONSchema { m := llmsdk.JSONSchema{} m["type"] = "object" m["properties"] = map[string]any{"id": map[string]any{"type": "string"}} m["required"] = []string{"id"} m["additionalProperties"] = false return m}func (t *ArtifactGetTool) Execute(_ context.Context, params json.RawMessage, _ Ctx, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var p GetParams if err := json.Unmarshal(params, &p); err != nil { return llmagent.AgentToolResult{}, err } fmt.Printf("[artifacts.get] id=%s\n", p.ID) a := t.S.Get(p.ID) body, _ := json.Marshal(map[string]interface{}{"artifact": a}) return llmagent.AgentToolResult{Content: []llmsdk.Part{llmsdk.NewTextPart(string(body))}, IsError: false}, nil}
type ArtifactListTool struct{ S *Store }
func (t *ArtifactListTool) Name() string { return "artifact_list" }func (t *ArtifactListTool) Description() string { return "List all documents" }func (t *ArtifactListTool) Parameters() llmsdk.JSONSchema { m := llmsdk.JSONSchema{} m["type"] = "object" m["properties"] = map[string]any{} m["additionalProperties"] = false return m}func (t *ArtifactListTool) Execute(_ context.Context, _ json.RawMessage, _ Ctx, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { fmt.Println("[artifacts.list]") body, _ := json.Marshal(map[string]interface{}{"artifacts": t.S.List()}) return llmagent.AgentToolResult{Content: []llmsdk.Part{llmsdk.NewTextPart(string(body))}, IsError: false}, nil}
type ArtifactDeleteTool struct{ S *Store }
func (t *ArtifactDeleteTool) Name() string { return "artifact_delete" }func (t *ArtifactDeleteTool) Description() string { return "Delete a document by id" }func (t *ArtifactDeleteTool) Parameters() llmsdk.JSONSchema { m := llmsdk.JSONSchema{} m["type"] = "object" m["properties"] = map[string]any{"id": map[string]any{"type": "string"}} m["required"] = []string{"id"} m["additionalProperties"] = false return m}func (t *ArtifactDeleteTool) Execute(_ context.Context, params json.RawMessage, _ Ctx, _ *llmagent.RunState) (llmagent.AgentToolResult, error) { var p DeleteParams if err := json.Unmarshal(params, &p); err != nil { return llmagent.AgentToolResult{}, err } fmt.Printf("[artifacts.delete] id=%s\n", p.ID) ok := t.S.Delete(p.ID) body, _ := json.Marshal(map[string]interface{}{"success": ok}) return llmagent.AgentToolResult{Content: []llmsdk.Part{llmsdk.NewTextPart(string(body))}, IsError: false}, nil}
const artifactsPrompt = `Use documents (artifacts/canvases) for substantive deliverables like documents, plans, specs, or code. Keep chat replies brief and status-oriented; put the full content into a document via the tools. Always reference documents by id.- Prefer creating/updating documents instead of pasting large content into chat- When asked to revise or extend prior work, read/update the relevant document- Keep the chat response short: what changed, where it lives (document id), and next steps`
type ArtifactsToolkit struct{ S *Store }
func (t ArtifactsToolkit) CreateSession(context.Context, Ctx) (llmagent.ToolkitSession[Ctx], error) { return &ArtifactsToolkitSession{S: t.S}, nil}
type ArtifactsToolkitSession struct{ S *Store }
func (s *ArtifactsToolkitSession) SystemPrompt() *string { prompt := artifactsPrompt return &prompt}
func (s *ArtifactsToolkitSession) Tools() []llmagent.AgentTool[Ctx] { return llmagent.FunctionTools[Ctx]( &ArtifactCreateTool{S: s.S}, &ArtifactUpdateTool{S: s.S}, &ArtifactGetTool{S: s.S}, &ArtifactListTool{S: s.S}, &ArtifactDeleteTool{S: s.S}, )}
func (*ArtifactsToolkitSession) Close(context.Context) error { return nil }
func main() { godotenv.Load("../.env") provider := os.Getenv("PROVIDER") if provider == "" { provider = "openai" } modelID := os.Getenv("MODEL") if modelID == "" { modelID = "gpt-5.6-terra" } model, err := examples.GetModel(provider, modelID, llmsdk.LanguageModelMetadata{}, "") if err != nil { log.Fatalf("Failed to create model: %v", err) }
store := NewStore()
agent := llmagent.NewAgent("artifacts", model, llmagent.WithToolkits[Ctx](ArtifactsToolkit{S: store}), )
// Demo: create then revise a product requirements document ctx := context.Background() items1 := []llmagent.AgentItem{ llmagent.NewAgentItemMessage(llmsdk.NewUserMessage(llmsdk.NewTextPart( "We need a product requirements document for a new Todo app. Please draft it in markdown with sections: Overview, Goals, Non-Goals, Requirements. Keep your chat reply short and save the full document to a separate document we can keep iterating on.", ))), } res1, err := agent.Run(ctx, llmagent.AgentRequest[Ctx]{Context: Ctx{}, Input: items1}) if err != nil { log.Fatal(err) } litter.Dump(res1.Content) fmt.Println("Documents after creation:") litter.Dump(store.List())
items2 := []llmagent.AgentItem{ llmagent.NewAgentItemMessage(llmsdk.NewUserMessage(llmsdk.NewTextPart( "Please revise the document: expand the Goals section with 3 concrete goals and add a Milestones section. Keep your chat reply brief.", ))), } res2, err := agent.Run(ctx, llmagent.AgentRequest[Ctx]{Context: Ctx{}, Input: items2}) if err != nil { log.Fatal(err) } litter.Dump(res2.Content) fmt.Println("Documents after update:") litter.Dump(store.List())}
func randID() string { return fmt.Sprintf("%x", time.Now().UnixNano())}