Code was always read more than written. With AI it shifts even more towards reading so language readability becomes even more important. And Ruby really shines there.
Btw, it's not even about the RubyLLM gem. The gem abstracts away the calling of various LLM providers and gives a very clean and easy to use interface. But it's not what gives the "agentic magic". The magic is pretty much all in the underlying LLMs.
Seeing all the claims made by some closed source agent products (remember the "world's first AI software engineer"?) I thought that a fair amount of AI innovation is in the agent tool itself. So I was surprised when I realised that almost all of the "magic" parts are coming from the underlying LLM.
It's also kind of nice because it means that if you wanted to work on an agent product you can do that even if you're not an AI specialised engineer (like I am not).
You can make it much more than just a coding agent. I personally use my personal LLMs for data analysis by integrating it with some APIs.
These type of LLM systems are basically acting as a frontend now that respond to very fuzzy user input. Such an LLM can reach out to your own defined functions (aka a backend).
The app space that I think is interesting and that I'm working on is creating these systems combined with some solid data creating advicing/coaching/recommendation systems.
If you want some input on building something like that, my email is in my profile. Currently I'm playing around with an LLM chat interface with database access that gives study advice based on:
* HEXACO data (personality)
* Motivational data (self-determination theory)
* ESCO data (skills data)
* Descriptions of study programs described in ESCO data
If you want to chat about creating these systems, my email is in my profile. I'm currently also looking for freelance opportunities based on things like this as I think there are many LLM applications to which we've only scratched the surface.
https://blog.scottlogic.com/2023/05/04/langchain-mini.html
It is of course quite out of date now as LLMs have native tool use APIs.
However, it proves a similar point to yours, in most applications 99% of the power is within the LLM. The rest is often just simple plumbing.
It's clearly not a full featured agent but the code is here and it's a nice starting point for a prototype: https://github.com/radanskoric/coding_agent
My best hope for it is that people will use it to experiment with their own ideas. So if you like it, please feel free to fork it. :)
Agents are the new PHP scripts!
Might poke around...
What makes something a good potential tool, if the shell command can (technically) can do anything - like running tests?
(or it is just the things requiring user permission vs not?)
The shell command can run anything really. When I tested it, it asked me multiple times to run the tests and then I could see it fixing the tests in iterations. Very interesting to observe.
If I was to improve this to be a better Ruby agent (which I don't plan to do, at least not yet), I would probably try adding some Rspec/Minitest specific tools that would parse the response and present it back to the LLM in a cleaned up format.
(Like - what would it look like to clean up test results for an LLM?)
I'm being serious. This sounds like a fun project but I have to turn my attention to other projects for the near future. This was more of an experiment for me, but it would be cool to see someone try out that idea.
Think of it as -semantic- wrappers so the LLM can -decide- what action to take at any given moment given its context, the user prompt, and available tools names and descriptions.
creating wrappers for the most used basic tools even if they all pipe to terminal unix commands can be useful.
also giving it speicif knowledge base it can consult on demand like a wiki of its own stack etc
but yeah
Basically, what I wanted to say was: "Here is an article on building a prototype coding agent in Ruby that explains how it works and the code is just 94 lines so you'll really be able to get a good understanding just by reading this article."
But that's a bit too long for a title. :)
When understanding a certain concept, it's very useful to be able to see just the code that's relevant to the concept. Ruby language design enables that really well. Also, Ruby community in general puts a lot of value on readability. Which is why with Ruby it's often possible to eliminate almost all of the boilerplate while still keeping the code relatively flexible.
Language expressiveness is more about making the interface support more use case while still being as concise. And Ruby is really good at this, better than most languages.
I suppose we have to define expressiveness (conciseness, abstraction power, readability, flexibility?), because Ruby, for example, has human-readable expressiveness, Common Lisp has programmable expressiveness, and Forth has low-level expressiveness, so they all have some form of expressiveness.
I think Ruby, Crystal, Rebol 3, and even Nim and Lua have a similar form or type of expressiveness.
If you say that expressivity is the ability to implement a program in less lines of code then Ruby is more expressive than most but less than for example Clojure. Well written Clojure can be incredibly expressive. However, you can argue that for most people it's going to be less readable than a comparable Ruby program.
It's hard to talk about these qualities as there's a fair amount of subjectivity involved.
I always put extra effort into trying to make my blog posts shorter without sacrificing the quality. I think good technical writing should transfer the knowledge while requesting the least amount of time possible from the reader.
Does that mean that it wouldn’t work with other LLMs?
E.g. I run Qwen3-14B locally; would that or any other model similar in size work?
Claude is just an example. I pulled the actual payloads by looking at what is actually being sent to Claude and what it is responding. It might vary slightly for other providers. I used Clause because I already had a key ready from trying it out before.
OpenAI launched function calls two years ago and it was always possible to create a simple coding agent.
When I realised that it's mostly in the LLM I found that a bit surprising. Also, since I'm not an AI Engineer, I was happy to realise that my "regular programming" skills would be enough if I wanted to build a coding agent.
It sounds like you were aware of that for a while now, but I and a lot of other people weren't. :)
That was my motivation for writing the article.
I wonder if AIs that receive this information within their prompt might try to change the user’s mind as part of reaching their objective. Perhaps even in a dishonest way.
To be safe I’d write “error: Command cannot be executed at the time”, or “error: Authentication failure”. Unless you control the training set; or don’t care about the result.
Interesting times.
Either the user needs to be educated or we need to restrict what the user themselves can do.
I took the code from RubyLLM configuration documentation. If you're pulling in a lot of config options and some have default values then there's value in symmetry. Using fetch with nil communicates clearly "This config, unlike those others, has no default value". But in my case, that benefit is not there so I think I'll change it to your suggestion when I touch the code again.
Using .fetch with a default of nil is what's arguably not very useful.
IMO it's just a robocop rule to use .fetch, which is useful in general for exploding on missing configuration but not useful if a missing value is handled.
Basically, what I wanted to say was: "Here is an article on building a prototype coding agent in Ruby that explains how it works and the code is just 94 lines so you'll really be able to get a good understanding just by reading this article."
But that's a bit too long for a title. :)
Also, a big benefit of Ruby is how concise it is. Yes, there's a library under neath my code but that library is, like Ruby, also very concise. That's kind of the point.
I can see how you might have read the title as hype-ish, but that was not my intention.
The imports indeed take away from that.
You can also do it without imports in about the same number of lines, if you strip it down a bit.
As CTO at Redo do you demand everything written in assembly? Does your entire company’s code run in one file or do you use abstraction to simplify?
I’m not quite clear on how expressing a really complex and paradigm shifting approach of agents in a more concise way is a bad thing
Ruby symbols (and atoms in Lisp / Erlang / Elixir) are a performance hack, they are basically "interned" strings. They're immutable and are mostly used for commonly-repeated values, e.g. names of keyword arguments, enum values etc. Unlike strings, they're supposed to be treated as a single, opaque value, with no other properties beyond the name they represent. Most languages don't give you APIs to uppercase or slice symbols, though you can usually convert to strings and back if you really need to.
The performance advantage of symbols/atoms is that they're represented as pointers or offsets into a global pool, which contains their names. This means repeated instances of the same symbol take up much less memory than they would as ordinary strings, as each instance is just a single pointer, and the actual name it represents is only stored once, in the global pool. Comparing symbols is also much faster than strings, as two symbols representing the same name will always have the same offset, turning an O(n) character-by-character comparison into a simple O(1) comparison of pointers.
It's really strange to me that so many high-level languages, which prize themselves on "developer happiness" and "mental compression" force you to think about strings in this way, out of all things.
I think there's something to be said for having a single character that can be used to represent identifiers as strings. Giving your internal string type an interned representation (with automatic interning of literals and a method to intern arbitrary strings) is also an interesting idea, though I know far too little about interpreter design to have an opinion on how much performance difference it would make. Conflating those two ideas together just seems like a really bad deal to me.
The other colon path: is used for named parameters/keyword args.
It’s the same syntax as a hash/dict, but as a parameter it does not have a default value in this case.
The old syntax, but still supported for hash/dict was “key => value”
Rust’s string slices are the equivalent of Ruby’s slices of an array: myarr[5..18]
The stated optimization from Matz (who created Ruby) is “developer happiness”
The important optimization for me is “fidelity to business logic”, eg less cruft and ruby syntactic sugar means you could sit at your computer and read your business rules (in code) out loud in real time and be understood by a non-dev
> why the "incomplete" colon?
Just ruby's syntax to denote 'keyword arguments', i.e. when calling this method, you must explicitly use the parameter name, like so: execute(path: "/some/path")
> class ListFiles < RubyLLM::Tool
> what's with the "<"?
Just ruby's way of denoting inheritance i.e. class A < B means class A inherits from B
> param :command, desc: "The command to execute"
> again, the colons are confusing...
The colons can be confusing, but you get used to them. The first one in :command is denoting a symbol. You could think of it as just a string. Any method that expects a symbol could be rewritten to uses a string instead, but symbols are a tad more elegant (one less keypress). The second one, desc:, is a keyword argument being passed to the param method. You could rewrite it as param(:command, desc: "The command to execute")
I sometimes wonder what ruby would be like without symbols, and part of me thinks the tradeoff would be worth it (less elegant, since there'd be "strings" everywhere instead of :symbols, but it would be a bit easer to learn/read.
1. They refer to the same object in memory, so multiple usages of a symbol by the same name (e.g. `:param`) not not add additional memory overhead.
2. Symbols are also immutable in that they cannot be mutated in any way once they are referenced/created.
These properties can be useful in some contexts, but in practice they're effectively used as immutable constant values.
Working with json/xml, or anything that has keys and text-values for that matter is a lot better with symbols
Some of the complaints here are just about Ruby syntax, which: shrug.
> (which itself is a lot more code added to 400 lines claimed by the author).
Most "X in n (<100) lines of code" projects posted on here mean just that. Import 2 million lines of code from libraries and just count the glue code lines.
It's marketing or something.
You say you "dabbled" a bit in Ruby and then proceed to demonstrate your lack of understading of basic Ruby syntax.
IMO it's because they mostly just don't think about the chaos they're doing. You encounter a function call that ends in a hash. Does the function being called use that as a hash or does it explode the hash to populate some extra trailing parameters? You have no way to know without going and looking. To me that's super dumb. To them it's just another day in unexpected behavior land.
Honestly, this is a feature. If the difference matters, then you are likely doing something wrong.
Pretty hard to grow and learn new concepts if you immediately label anything you don’t understand yet as “super dumb”
I assume this is referring to passing hashes in as parameters to methods. Ruby 3 made this more explicit; you must use ** to convert a hash into positional arguments and you must use {} around your key/values if the first argument is a hash.
Also Rubyists as a whole tend to update to the newest Ruby fairly fast. There are of course places that don't upgrade quickly (especially legacy systems that are barely touched), but most places with living code bases seems to be very quick when it comes to updating nowadays.