I built a coding agent back in 2022, 2 months before ChatGPT launched:

It’s super cool how I have come full circle. back in those days, we didn’t have tool calling, reasoning, not even GPT 3.5!

It used code-davinci-002 in a custom Jupyter kernel, a.k.a. the OG codex code completion model. The kids these days probably have not seen the original Codex launch video with Ilya, Greg and Wojciech. If you have time, sit down to watch and realize how far we’ve come since August 2021, airing of that demo 4.5 years ago.

For some reason, I did not even dare to give codex bash access, lest it delete my home folder. So it was generating and executing Python code in a custom Jupyter kernel.

This meant that the conversations were using Jupyter nbformat, which is an array of cell input/output pairs:

{
  "cells": [
    {
      "cell_type": "code",
      "source": "<Input 1>",
      "outputs": [
         ... <Outputs 1>
      ]
    },
    {
      "cell_type": "code",
      "source": "<Input 2>",
      "outputs": [
         ... <Outputs 2>
      ]
    }
  ]
}

In fact, this product grew into TextCortex’s current chat harness over time. After seeing ChatGPT launch, I repurposed icortex in a week into Flask to use text-davinci-003 and we had ZenoChat, our own ChatGPT clone, before Chat Completions was in the API (it took them some months). It did not even have streaming, since Flask does not support ASGI.

As it turns out, nbformat is not the best format for a conversation. Instead of input/output pairs, OpenAI data model used an tree of message objects, each with a role: user|assistant|tool|system and a content field which could host text, images and other media:

{
  "mapping": {
    "client-created-root": {
      "id": "client-created-root",
      "message": null,
      "parent": null,
      "children": ["user-1"]
    },
    "user-1": {
      "id": "user-1",
      "message": {
        "id": "user-1",
        "author": { "role": "user", ... },
        "content": "Hello"
      },
      "parent": "client-created-root",
      "children": ["assistant-1"]
    },
    "assistant-1": {
      "id": "assistant-1",
      "message": {
        "id": "assistant-1",
        "author": { "role": "assistant", ... },
        "content": "Hi"
      },
      "parent": "user-1",
      "children": []
    }
  },
  "current_node": "assistant-1"
}

You will notice that the data model they serve from the API is an enriched version of the deprecating ChatCompletions API. Eg. whereas ChatCompletions role is a string, in OpenAI’s own backend has the author object that can store name, metadata, and other useful stuff for each entity in the conversation.

After reverse engineering it, I copied it to be TextCortex’s new data model, which it still remains, with some modifications.

I thought the tree structure being used to emulate message editing experience was very cool back in the days. OpenAI’s need for human annotation for later training and the user’s need for getting a different output, two birds in one stone.

Now I don’t know what to think of it, since CLI coding agents like Codex and Claude Code don’t have branching, just deleting back to a certain message. A part of me still misses branching in these CLI tools.

When I made icortex,

  • we were still 8 months away (May 2023) from the introduction of “tool calling” in the API, or as it was originally called, “function calling”.
  • we were 2 years away (Sep 2024) from the introduction of OpenAI’s o1, the first reasoning model.

both of which were required to make current coding agents possible.

In the video above, you can even see the approval [Y/n] gate before executing. I was so cautious, for some reason, presumably because smol-brained model generated the wrong thing 80% of the time. It is remarkable how much it resembles Claude Code, after all this time.

Definition of being too early…


Repo: github.com/textcortex/icortex