Built with Jac

Agentic AI in four primitives

A hackathon pitch builder that brainstorms ideas, structures them into a typed pitch, researches them with real tools, and routes them to the right expert mentors — each stage one small Jac construct. Read the four snippets, then run the whole thing as a full-stack app.

quickstart
curl -fsSL https://raw.githubusercontent.com/jaseci-labs/jaseci/main/scripts/install.sh | bash -s -- --standalone
export PATH="$HOME/.local/bin:$PATH"

jac install                          # resolves the LLM capability into .jac/venv
export OPENAI_API_KEY="your-key"

jac start app.jac                    # -> http://localhost:8000

The four primitives

Each one is a single language construct. Together they cover most of what an agent needs to do.

Snippets

Every file below runs on its own with jac run — no server, no UI, just the primitive. This is the exact source in the repo.

01

Generate

by llm()

Write a signature, skip the body. The function name, parameter names, return type and its sem description become the prompt.

step1_generate.jac
"""Step 1: Generate — the `by llm()` pattern.
The function signature (name + params + return type + docstring) IS the prompt.
No function body needed — the LLM fills it in at runtime.

Run: jac run step1_generate.jac
"""

def:pub brainstorm_ideas(interests: str, skills: str) -> str by llm();

sem brainstorm_ideas = "Brainstorm 3 creative hackathon project ideas based on the person's interests and skills. For each idea give it a catchy name and a one-sentence description. Make them fun, feasible in 24 hours, and genuinely useful.";

with entry {
    ideas = brainstorm_ideas(
        interests="music, coffee, sustainability",
        skills="Python, React"
    );
    print("=== Step 1: Generate ===\n");
    print(ideas);
}
$jac run step1_generate.jac
output
1. **EcoTune**: An interactive platform that curates personalized music
   playlists based on your coffee brewing process and sustainability goals.

2. **BrewCycle**: A web app connecting local coffee shops with customers
   looking to trade surplus coffee grounds for plants or compost.

3. **Melody Mug**: A coffee mug that plays a soothing soundscape when
   filled, encouraging mindful breaks while reducing waste.
Why it matters. No prompt string, no API client, no response parsing. The signature is the interface.
02

Extract

typed return

Same as Generate, but the return type is a typed obj. The compiler enforces the schema, so the model cannot hand back malformed data.

step2_extract.jac
"""Step 2: Extract — typed return from `by llm()`.
Same as Generate, but returns a typed obj instead of str.
The compiler enforces the schema — no JSON parsing, no "parse and pray."

Run: jac run step2_extract.jac
"""

enum Difficulty { BEGINNER, INTERMEDIATE, ADVANCED }
enum Track { WEB, MOBILE, AI_ML, GAME, OTHER }

obj HackathonPitch {
    has title: str;
    has problem: str;
    has solution: str;
    has tech_stack: list[str];
    has wow_factor: str;
    has difficulty: Difficulty;
    has track: Track;
}

def:pub structure_pitch(raw_idea: str) -> HackathonPitch by llm();

sem structure_pitch = "Turn a raw hackathon idea into a structured, compelling pitch with a title, problem statement, solution, tech stack, wow factor, difficulty, and track.";

with entry {
    pitch = structure_pitch(
        "An app that matches leftover restaurant food with nearby shelters in real time"
    );
    print("=== Step 2: Extract ===\n");
    print(f"Title:       {pitch.title}");
    print(f"Problem:     {pitch.problem}");
    print(f"Solution:    {pitch.solution}");
    print(f"Tech stack:  {pitch.tech_stack}");
    print(f"Wow factor:  {pitch.wow_factor}");
    print(f"Difficulty:  {pitch.difficulty}");
    print(f"Track:       {pitch.track}");
}
$jac run step2_extract.jac
output
Title:       Food Rescue Connect
Problem:     Every day, restaurants throw away tons of edible food while
             nearby shelters struggle to feed the hungry.
Solution:    A mobile app that lets restaurants list leftover food in real
             time, which nearby shelters can claim.
Tech stack:  ['React Native', 'Node.js', 'Express', 'MongoDB', 'Google Maps API']
Difficulty:  Difficulty.INTERMEDIATE
Track:       Track.MOBILE
Why it matters. enum fields make invalid values unrepresentable — difficulty can only ever be one of three values.
03

Invoke

tools=[...]

Hand the model callable Jac functions. It runs a ReAct loop on its own: reason, call a tool, observe the result, repeat until it has enough.

step3_invoke.jac
"""Step 3: Invoke — `by llm(tools=[...])`.
Give the LLM callable functions. It runs a ReAct loop:
reason → call tool → observe result → repeat until done.

Run: jac run step3_invoke.jac
"""

import from tools { search_github, describe_tech_stack, estimate_build_time }

def:pub research_idea(idea: str) -> str by llm(
    tools=[search_github, describe_tech_stack, estimate_build_time]
);

sem research_idea = "Research a hackathon project idea thoroughly before answering. Use the tools to find real data.";

with entry {
    result = research_idea(
        "AI-powered accessibility tool that generates live captions for deaf users at events"
    );
    print("=== Step 3: Invoke ===\n");
    print(result);
}
$jac run step3_invoke.jac
output
### Similar Open-Source Projects on GitHub
  - live-caption/live-caption  — 2.1k stars
  - openai/whisper             — 68k stars

### Recommended Tech Stack
  - Frontend: React + WebSockets for live caption streaming
  - Backend:  Python (FastAPI), Whisper for speech-to-text

### Estimated Build Time
  - Duration: 24-36 hours with a team size of 4.
Why it matters. Tools are ordinary functions. sem tells the model when to reach for each one; it decides the order and when to stop.
04

Route + Spawn

visit [-->] by llm()

The graph is the routing table. The model reads each node's description and visits the ones that fit. Every selected node spawns a worker that runs in parallel.

step4_route.jac
"""Step 4: Route + Spawn — `visit [-->] by llm()` + `flow spawn` + `wait`.
The LLM reads node descriptions and picks the best expert(s).
Each selected node flow-spawns a parallel AdviceWorker.
Walker collects all results by awaiting each with `wait`.

No if/else chains. The graph topology IS the routing table.

Run: jac run step4_route.jac
"""

# Worker walker — one per selected mentor, runs concurrently
walker AdviceWorker {
    has pitch: str;
    has mentor: str;
    has advice: str = "";

    can work with Root entry {
        self.advice = get_advice(self.pitch, self.mentor);
    }
}

def:pub get_advice(pitch: str, mentor: str) -> str by llm();

sem get_advice = "Give practical next-step advice for building this hackathon project. Suggest 3-5 concrete first tasks.";

# Expert nodes — LLM reads `description` to decide which ones to visit
node WebDevMentor {
    has description: str = "Expert in web apps: React, APIs, databases, authentication, and full-stack deployment";

    can respond with HackathonAdvisor entry {
        t = flow root spawn AdviceWorker(pitch=visitor.pitch, mentor="Web Dev");
        visitor.tasks = visitor.tasks + [t];
        visitor.mentors = visitor.mentors + ["Web Dev"];
    }
}

node MobileMentor {
    has description: str = "Expert in mobile apps: iOS, Android, React Native, Expo, and mobile UX";

    can respond with HackathonAdvisor entry {
        t = flow root spawn AdviceWorker(pitch=visitor.pitch, mentor="Mobile");
        visitor.tasks = visitor.tasks + [t];
        visitor.mentors = visitor.mentors + ["Mobile"];
    }
}

node AIMLMentor {
    has description: str = "Expert in AI/ML: LLMs, embeddings, computer vision, and model APIs like OpenAI";

    can respond with HackathonAdvisor entry {
        t = flow root spawn AdviceWorker(pitch=visitor.pitch, mentor="AI/ML");
        visitor.tasks = visitor.tasks + [t];
        visitor.mentors = visitor.mentors + ["AI/ML"];
    }
}

node GameDevMentor {
    has description: str = "Expert in game dev: Unity, Godot, Pygame, game design, and interactive experiences";

    can respond with HackathonAdvisor entry {
        t = flow root spawn AdviceWorker(pitch=visitor.pitch, mentor="Game Dev");
        visitor.tasks = visitor.tasks + [t];
        visitor.mentors = visitor.mentors + ["Game Dev"];
    }
}

node CollectNode {}  # walker walks here after all mentor visits to await workers

walker HackathonAdvisor {
    has pitch: str;
    has tasks: list = [];    # flow spawn handles (awaited with `wait`)
    has mentors: list = [];  # parallel list: mentor name per task
    has results: list = [];

    can route with Root entry {
        collect = CollectNode();
        here ++> WebDevMentor() ++> collect;
        here ++> MobileMentor() ++> collect;
        here ++> AIMLMentor() ++> collect;
        here ++> GameDevMentor() ++> collect;
        # Route: LLM reads each node's description, visits the best match(es).
        # Each visited node flow-spawns an AdviceWorker — all run in parallel.
        visit [-->] by llm(incl_info={"Hackathon pitch": self.pitch});
        # `visit` is deferred, so results can't be collected here — the walker
        # arrives at CollectNode only after every selected mentor has run.
        visit collect;
    }

    can collect with CollectNode entry {
        # Await each parallel worker and collect results
        for i in range(len(self.tasks)) {
            w = (wait self.tasks[i]) as AdviceWorker;
            self.results = self.results + [{"mentor": self.mentors[i], "advice": w.advice}];
        }
    }
}

with entry {
    advisor = root spawn HackathonAdvisor(
        pitch="A real-time multiplayer trivia game with AI-generated questions using WebSockets and React"
    );
    print("=== Step 4: Route + Spawn ===\n");
    for r in advisor.results {
        print(f"--- {r['mentor']} ---");
        print(r["advice"]);
        print("");
    }
}
$jac run step4_route.jac
output
=== Step 4: Route + Spawn ===

--- Web Dev ---
1. Set up the project repository and initialize a React application.
2. Research and integrate a WebSocket library (e.g. Socket.IO).
...

--- AI/ML ---
1. Implement a basic AI model or API for generating trivia questions.
...

--- Game Dev ---
1. Design the basic UI layout: questions, answer options, player scores.
Why it matters. No if/else dispatch. Add a mentor by adding a node — the routing logic never changes.

The full app

The same four primitives wired into a full-stack app — server logic in app.jac, React-style client components in frontend/. One language, one command.

run it
jac start app.jac
# -> http://localhost:8000

# walkers are exposed as HTTP endpoints automatically:
#   POST /walker/run_brainstorm   {interests, skills}
#   POST /walker/run_structure    {raw_idea, interests, skills}
#   POST /walker/run_research     {idea, title, solution}
#   POST /walker/run_route        {title, problem, solution, ...}

What the browser does

  1. Generate — you enter interests and skills; the agent brainstorms three ideas.
  2. Extract — the idea you pick becomes a typed pitch with a tech stack and track.
  3. Invoke — the agent searches GitHub and estimates build time with real tools.
  4. Route — mentors are chosen by the model and answer in parallel.

How the client calls the server

A client component imports server walkers directly and spawns them — the HTTP call, serialization and typing are generated for you.

sv import from ...app { run_brainstorm }

async def do_brainstorm() -> None {
    result = root spawn run_brainstorm(
        interests=interests, skills=skills
    );
    ideas = result.reports[0];
}