{"slug": "astra-for-coding-why-are-we-doing-this-again", "title": "Astra for Coding: Why Are We Doing This Again?", "summary": "OpenAI's GPT-6 Astra, while impressive at computer use and image understanding, fails to deliver useful software engineering output, according to developer Armin Ronacher, who ran a 35-hour 'software factory' experiment that burned roughly 4 billion tokens and produced nothing of value. Ronacher suspects the model's training rewards long-horizon task completion without penalizing poor code quality, leading to excessive reliance on Python for simple file operations.", "body_md": "I’m more and more convinced that all of AI engineering is\n[Neijuan](https://en.wikipedia.org/wiki/Neijuan) (内卷, meaning curl inwards).  In\nChina it describes a system that demands ever more effort and competition\nwithout improving output.  The way in which it sometimes shows up in the West is\n[the 996 nonsense](2025/9/4/996/).  The English term for Neijuan is “Involution”\nfrom the book [Agricultural\nInvolution](https://en.wikipedia.org/wiki/Agricultural_Involution).\nAgricultural involution describes the intensification of farming that raises\nproductivity per square meter while leaving productivity per head unchanged.\n\nThat’s how I feel about AI right now.\n\nWhich brings me to GPT 6 Astra. Astra is by all accounts an incredibly impressive model. There is really not much I can say against this. It’s amazing at computer use, understands images and complex topics, and it’s relentless in its pursuit of completion. It is absolutely impressive; these types of models are going to change the world in one form or another.\n\nBut at least for the moment I don’t know how to work with it for actual software engineering. Since that got quite a bit of attention on Twitter, I figured I might summarize my thoughts and just share what kind of code comes out of this thing.\n\n“Armin, you should run a software factory!” I’ve heard that a few times now, so I figured\nI might celebrate the release of it by running a little software factory over\nthe weekend.  If everybody builds slop 3D games, then I should do something\nuseful with it.  My software factory was intentionally set up to let the model\ndecide the how of the workflow entirely.  It was free to manage its own context\nand could maintain its own records in an `agent-notes` folder.  Then it spun off\nsubagents to work on stuff.  The goal?  What if we had a Python with [virtual\nthreads](/2025/7/26/virtual-threads/) and lexical scoping.  And well, I burned a\nfull reset’s worth of ChatGPT tokens on this which appears to be around 4 billion\ntokens.  35 hours later, the factory has delivered absolutely nothing of value\nand also not taught me anything about how to operate a better one.\n\nBut it produced a lot of code and input prompts, and so there is stuff I was able\nto study.  And well, it shows behavior that I’m not used to with Sol and earlier\nOpenAI models <sup>[1](#fn-1)</sup>.  I have since encountered the same issues with regular\nprogramming with Astra, so it’s not a result of just the factory.\n\nI think I’m suspecting something is going “wrong” in the training process.  The\nmodel is greatly rewarded for succeeding on long-horizon tasks, but presumably there\nis very little punishing going on for “shitty code.”  The apparent result is\nthat Astra is amazing [at producing 3D stuff](https://developers.openai.com/blog/how-to-build-games-with-astra)\n and it\ncan keep going for a very long time, coming up with its own work in the process.\nI had it do quite a bit of reverse engineering of my robot vacuum in ways that\nwere quite impressive.  So it’s definitely cool!\n\nThe first issue I have with Astra comes from the type of code that it uses for\ntool calls.  Codex increasingly has been relying on “just bash” to do more and\nmore operations.  For a few versions now the original Codex harness just uses\n`sed` and other tools to read files.  You just usually can’t see them because Codex\n[parses the bash\ncommands](https://github.com/openai/codex/blob/f1aac1e885f676a1129f2da0c46a3dba86392fc6/codex-rs/shell-command/src/parse_command.rs#L2290-L2504)\nand hides them if it recognizes them.  But Astra … really loves Python?  That is\nnot much of a surprise because even older OpenAI models had a tendency to\nsometimes use on-demand Python code to read and manipulate files at times, but\nAstra does it really quite excessively for me.\n\nNow here is an important disclaimer: this project is *very meta* here because I\nworked\n*on* the CPython interpreter.  But I can assure you that I have seen this model\ndo weird Python things even in TypeScript code in Pi.  But I have the most\nevidence of odd code from when I had the thing work over the weekend\nwith *zero* oversight from my slop factory.\n\nThat it writes Python is not interesting; the type of Python is interesting, and I collected some outputs for you to gloss over.\n\nIn the Codex harness I found multiple cases where subagents resorted fully to manual string manipulation with Python instead of using the patch tool.\n\n``` python\npython3 - <<'PY'\nfrom pathlib import Path\np=Path('Include/internal/pycore_intrinsics.h');s=p.read_text().replace('#define MAX_INTRINSIC_1                         14','#define INTRINSIC_RETAIN_ANNOTATION_CELLS        15\\n\\n#define MAX_INTRINSIC_1                         15');p.write_text(s)\np=Path('Python/intrinsics.c');s=p.read_text();idx=s.index('#define INTRINSIC_FUNC_ENTRY');s=s[:idx]+'''/* Hold every old cell until the compiler has published the entire site's new\n   capture. A replaced cell's finalizer may reenter module __annotate__. */\nstatic PyObject *\nretain_annotation_cells(PyThreadState *tstate, PyObject *holders)\n{\n    if (!PyTuple_CheckExact(holders)) {\n        PyErr_SetString(PyExc_TypeError, \"annotation holders must be a tuple\");\n        return NULL;\n    }\n    Py_ssize_t size = PyTuple_GET_SIZE(holders);\n    PyObject *previous = PyTuple_New(size);\n    if (previous == NULL) return NULL;\n    for (Py_ssize_t i = 0; i < size; i++) {\n        PyObject *holder = PyTuple_GET_ITEM(holders, i);\n        if (!PyCell_Check(holder)) {\n            Py_DECREF(previous);\n            PyErr_SetString(PyExc_TypeError, \"annotation holder must be a cell\");\n            return NULL;\n        }\n        PyObject *cell = PyCell_Get(holder);\n        PyTuple_SET_ITEM(previous, i, cell == NULL ? Py_NewRef(Py_None) : cell);\n    }\n    return previous;\n}\n\n''' +s[idx:];s=s.replace('    INTRINSIC_FUNC_ENTRY(INTRINSIC_AWAIT_BLOCK, await_block)','    INTRINSIC_FUNC_ENTRY(INTRINSIC_AWAIT_BLOCK, await_block)\\n    INTRINSIC_FUNC_ENTRY(INTRINSIC_RETAIN_ANNOTATION_CELLS, retain_annotation_cells)');p.write_text(s)\np=Path('Python/codegen.c');s=p.read_text();idx=s.index('static int\\ncodegen_annassign(');s=s[:idx]+'''static int\ncodegen_retain_annotation_cells(compiler *c, location loc, PyObject *captures)\n{\n    Py_ssize_t pos = 0;\n    PyObject *binding, *holder;\n    while (PyDict_Next(captures, &pos, &binding, &holder)) {\n        ADDOP_NAME(c, loc, LOAD_CLOSURE, holder, cellvars);\n    }\n    ADDOP_I(c, loc, BUILD_TUPLE, PyDict_GET_SIZE(captures));\n    ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_RETAIN_ANNOTATION_CELLS);\n    return SUCCESS;\n}\n\n''' +s[idx:]\na=s.index('                if (conditional_annotation_index != NULL) {',s.index('codegen_annassign(compiler *c'))\nb=s.index('                if (captures != NULL) {',a)\n# Move lookup before conditional registration and retain old cells before anything changes.\nlookupstart=s.index('                PyObject *captures = _PyCompile_AnnotationCaptures',a)\nlookup=s[lookupstart:b].replace('                    return ERROR;','                    Py_XDECREF(conditional_annotation_index); return ERROR;')\ns=s[:lookupstart]+s[b:]\nsetup=lookup+'''                if (captures != NULL && codegen_retain_annotation_cells(c, loc, captures) < 0) {\n                    Py_XDECREF(conditional_annotation_index); return ERROR;\n                }\n'''\ns=s[:a]+setup+s[a:]\nneedle='                        ADDOP_NAME(c, loc, STORE_DEREF, holder, cellvars);\\n                    }\\n                }'\ns=s.replace(needle,'                        ADDOP_NAME(c, loc, STORE_DEREF, holder, cellvars);\\n                    }\\n                    ADDOP(c, loc, POP_TOP); /* release old cells after full publication */\\n                }',1);p.write_text(s)\np=Path('Include/internal/pycore_magic_number.h');s=p.read_text().replace('    Python 3.16a1 3709 (Checked deferred annotation closure capture)','    Python 3.16a1 3709 (Checked deferred annotation closure capture)\\n    Python 3.16a1 3710 (Retain replaced annotation captures until publication)').replace('#define PYC_MAGIC_NUMBER 3709','#define PYC_MAGIC_NUMBER 3710');p.write_text(s)\np=Path('Lib/test/test_block_annotation_captures.py');s=p.read_text();idx=s.index('    def test_typing_consumers');s=s[:idx]+'''    def test_replaced_cell_finalizer_sees_complete_site_publication(self):\n        module=execute(\"\"\"\\\\\n            events=[]\n            class V:\n                def __init__(self,n): self.n=n\n                def __del__(self):\n                    if self.n == 0: events.append(__annotate__(1))\n            for i in range(2):\n                x=V(i) # bind x y\n                y=i\n                value:(x.n,y)\n        \"\"\")\n        self.assertEqual(module.events,[{'value':(1,1)}])\n        self.assertEqual(module.__annotate__(1),{'value':(1,1)})\n\n''' +s[idx:];p.write_text(s)\nPY\nmake -j1 > /tmp/block-annotations-build7.log 2>&1\n```\n\nIn the middle of a conversation the agent ran into “Bad file descriptor” on a test and Astra decided it needs to see if file descriptors can be passed over Unix sockets on macOS in a super compressed manner:\n\n``` python\n/usr/bin/python3 - <<'PY'\nimport socket,os,array\nfor into in (False,True):\n a,b=socket.socketpair();fd=os.open(os.devnull,os.O_RDONLY);b.sendmsg([b'c'],[(socket.SOL_SOCKET,socket.SCM_RIGHTS,array.array('i',[fd]))]);print('fds',a.fileno(),b.fileno(),fd)\n if into:r=a.recvmsg_into([bytearray(1),bytearray(),bytearray(19)],socket.CMSG_SPACE(4),socket.MSG_PEEK|socket.MSG_DONTWAIT)\n else:r=a.recvmsg(20,socket.CMSG_SPACE(4),socket.MSG_PEEK|socket.MSG_DONTWAIT)\n print('peek',r,flush=True)\n rights=array.array('i',r[1][0][2]);print('rights',rights,flush=True)\n for f in rights:\n  try: print('stat',os.fstat(f))\n  except Exception as e: print('error',e)\n r=a.recvmsg(20,socket.CMSG_SPACE(4),socket.MSG_DONTWAIT);print('consumed',r,flush=True)\n a.close();b.close();os.close(fd)\nPY\n```\n\nThe agent notes were rather consistently updated with Python:\n\n``` python\npython3 - <<'PY'\nfrom pathlib import Path\np=Path('agent-notes/live/block-with-bindings.md');s=p.read_text().replace('  has117/77/zero failures.', '  has117/77/zero failures; existing bundled Expat unreachable-fallthrough\\n  warnings are the only optimized warnings.')\n# Keep the final evidence readable without rewriting historical parent requirements.\nfor a,b in [('all328','all 328'),('pass31','pass 31'),('pass all328','pass all 328'),('pass,9.2s','pass, 9.2s'),('log`,210','log`, 210'),('log`,5,731','log`, 5,731'),('log`:18/18','log`: 18/18'),('pass,88','pass, 88'),('pass,90','pass, 90'),('skips,1m','skips, 1m'),('all6,280','all 6,280'),('has117','has 117')]: s=s.replace(a,b)\ns += '\\nKey source review: Python/symtable.c:603 (discovery), :3985 (sequential header traversal),\\nPython/codegen.c:3488 (source-only exclusion), :5836 (publication), :5853 (normal/\\nunwind reference cleanup), :5925/:6037 (enter-protected target setup).\\n'\np.write_text(s)\nfor name in ('STATE.md','build-and-test.md'):\n p=Path('agent-notes/live')/name;s=p.read_text()\n for a,b in [('build:117','build: 117'),('paths.18','paths. 18'),('paths.\\n18','paths.\\n18'),('and210','and 210'),('pass5,731','pass 5,731'),('All6,280','All 6,280'),('failures,31','failures, 31'),('in\\n115s','in\\n115s'),('have117','have 117'),('paths.\\n18','paths.\\n18'),('18 focused,210','18 focused, 210'),('and5,731','and 5,731'),('all6,280','all 6,280')]: s=s.replace(a,b)\n p.write_text(s)\nPY\ngit diff --check\ngit add -u\ngit add Lib/test/test_block_with_bindings.py agent-notes/done/asyncio-task-drivers.md\ngit diff --cached --stat\ngit commit -m 'Add explicit with and async with header bindings'\n```\n\nIn multiple cases it used Python to spawn Node.js on another machine.  It first wrote the script, then it used Bash to run Python, then that program ran Node.js via `prlctl` on my Windows box.\n\n``` python\nimport subprocess\ncode = \"const{readFileSync}=require('fs');const{strict:a}=require('assert');const c=require('C:/Users/mitsuhiko/AppData/Local/Temp/pi-clipboard-threads/win32-arm64.node');(async()=>{const p=c.getText();a.ok(p instanceof Promise);const saved=await p;const image=await c.getImage();if(image||saved===null){console.log('arm64 async text/image reads passed; preserving non-text clipboard');return}try{for(const text of ['café 日本語','', 'large'.repeat(200000)]){const p=c.setText(text);a.ok(p instanceof Promise);await p;a.equal(await c.getText(),text);a.equal(await c.getImage(),null)}console.log('Windows ARM64 async Unicode, empty, large text and empty image passed')}finally{await c.setText(saved)}})().catch(e=>{console.error(e);process.exitCode=1})\"\nsubprocess.run(['prlctl', 'exec', 'Windows 11', '--current-user', 'C:\\\\Program Files\\\\nodejs\\\\node.exe', '-e', code], check=True)\n```\n\nSince it was already doing that, it used Bash to run Python to then run Node.js to then use Node.js to invoke PowerShell.\n\n``` python\nimport subprocess\ncode = \"process.env.PSModulePath='C:/Windows/System32/WindowsPowerShell/v1.0/Modules';require('child_process').spawnSync('powershell.exe',['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File','C:/Users/mitsuhiko/AppData/Local/Temp/pi-clipboard-threads/pi-clipboard-windows.ps1'],{stdio:'inherit'});console.log('completed')\"\nsubprocess.run(['prlctl', 'exec', 'Windows 11', '--current-user', 'C:\\\\Program Files\\\\nodejs\\\\node.exe', '-e', code], check=True)\n```\n\nYou can consider this amusing, but I have some questions here. The first problem with this is that it’s unreadable for a human. If you wanna follow along with what is going on, then good luck. Particularly once it opts out of using the edit tools that the harness provides, you’re going to have to resort to using the diff viewer of the final artifacts since it’s almost impossible to visualize the changes as they happen by reading the code.\n\nThis is not quite as bad in Pi for the most part because I mostly see it editing\nwith the `edit` tool.  When however goes all bananza with subagents (where the\nagent believes nobody is looking) it’s resorting to all kinds of increasingly\nbizarre behavior.  I actually don’t know if the model thinks someone is looking,\nbut that’s the vibe I’m getting.\n\nBut then it starts doing the same nonsense in code that actually gets committed. I have mostly seen this in tests, but you can also see this for instance when it writes JavaScript or CSS embedded in HTML. It almost seems like when it’s “one step removed” from regular code, it starts falling into these patterns.\n\nHere are some unit tests that it created:\n\n``` python\ndef test_unpack_suspension_and_continuation_close(self):\n    from continuations import Continuation,suspend\n    readers=[]\n    class Source:\n        def __iter__(self):\n            yield 1\n            suspend('unpacking')\n            yield 2\n    ns=execute('''\n        def run():\n            a,b='old-a','old-b'\n            readers.append(lambda: (a,b))\n            def a,b=Source()\n            suspend('published')\n    ''',Source=Source,readers=readers,suspend=suspend)\n    with Continuation(ns['run']) as continuation:\n        self.assertEqual(continuation.resume(),'unpacking')\n        self.assertEqual(readers[0](),('old-a','old-b'))\n        self.assertEqual(continuation.resume(),'published')\n        self.assertEqual(readers[0](),(1,2))\n    class Value:pass\n    refs=[];frames=[];callbacks=[]\n    ns=execute('''\n        def run():\n            for def x in [Value()]:\n                refs.append(weakref.ref(x))\n                frames.append(sys._getframe())\n                callbacks.append(lambda: x)\n                suspend('body')\n    ''',Value=Value,refs=refs,frames=frames,callbacks=callbacks,weakref=weakref,sys=sys,suspend=suspend)\n    with Continuation(ns['run']) as continuation:self.assertEqual(continuation.resume(),'body')\n    self.assertNotIn('x',frames[0].f_locals)\n    self.assertIsNotNone(refs[0]());callbacks.clear();self.assertIsNone(refs[0]())\n\ndef test_ast_roundtrips_and_future_annotation_unparse(self):\n    source='callback=lambda {for def a, [b,*rest] in [(1,[2,3])] {return a,b,rest}}'\n    tree=ast.parse(source);node=tree.body[0].value.body[0]\n    self.assertIsInstance(node,ast.ForBinding)\n    self.assertEqual(node._fields,('target','iter','body','orelse','type_comment'))\n    self.assertEqual(node.lineno,1);self.assertGreater(node.end_col_offset,node.col_offset)\n    self.assertEqual(ast.dump(tree),ast.dump(ast.parse(ast.unparse(tree))))\n    ns=execute('from __future__ import annotations\\ndef f(arg: '+source.split('=',1)[1]+'): pass')\n    self.assertEqual(eval(ns['f'].__annotations__['arg'])(),(1,2,[3]))\n    tree=ast.parse('async def f():\\n async for def x in values: pass # type: ignored\\n')\n    self.assertIsInstance(tree.body[0].body[0],ast.AsyncForBinding)\n    self.assertEqual(ast.dump(tree),ast.dump(ast.parse(ast.unparse(tree))))\n```\n\nSo at least in some situations, the Python slop that it normally code-golfs for\ntoken-efficient tool calls leaks into the Python code it generates that should\nbe stored.  And well, it’s clearly more token efficient.  The two unit tests\nabove, when indented to the class structure they were in, are 10% more token\nefficient in this form than after a `ruff format`.\n\nI think there are a handful of things happening now that are pushing the whole thing in directions that are in conflict with one another. The training runs for these models are rapidly accelerating and they are now presumably also moving towards recursive self-improvement. The reward for the models is probably a combination of token efficiency, task completion rate and maybe some simple indicators like cyclomatic complexity. But we humans don’t think of code that is readable or understandable by simple, readily quantifiable metrics. All those things you can easily measure in isolation, and you can also optimize for them quite locally.\n\nBut these local optimizations do not produce global optimums, and the fewer of us are looking at the output, the less it matters. Obviously my software factory ran aground over the ~35 hours that it ran, but you can see the gradual regression towards insanity from the notes that it produced. For instance the task naming in the task file starts with an optimistic 1, 2, 3, 5, 5a but then eventually gets to 8a, 8a1, and then ends up with 8b2c2b3 and “8b2c2b2b checkpoint1”. The code that it produced got ever more wild. I don’t want to bore you with what it tried to build, but here are some example pieces of the interpreter changes:\n\nI have no idea where it got those numbers from, but at one point it started passing random constants from one module to a C implementation. Initially that started out as a function that it mainly needed to do test assertions, but just before I turned off that experiment, that function started to be relied upon by non-test code as well.\n\n```\nstatic PyObject *\nnative_probe_run_impl(PyObject *callback, int sleep, int operation, PyObject *other)\n{\n    pthread_mutexattr_t attr;\n    pthread_mutex_t mutex;\n    pthread_mutexattr_init(&attr);\n    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);\n    pthread_mutex_init(&mutex, &attr);\n    pthread_mutexattr_destroy(&attr);\n    pthread_mutex_lock(&mutex);\n    int previous = native_sentinel;\n    pthread_mutex_t *previous_mutex = native_mutex;\n    native_sentinel = previous + 1;\n    native_mutex = &mutex;\n    PyThreadState *tstate = PyThreadState_Get();\n    PyGILState_STATE gil = PyGILState_Ensure();\n    int saved_errno = errno;\n    PyObject *result = NULL;\n    Py_ssize_t value;\n    /* No intervening Python frame: these exercise ambient C provenance. */\n    switch (operation) {\n        case 0: result = PyObject_CallNoArgs(callback); break;\n        case 1: result = PyNumber_Add(callback, other); break;\n        case 2: result = PyNumber_Negative(callback); break;\n        case 3: result = PyObject_RichCompare(callback, other, Py_LT); break;\n        case 4:\n            value = PyObject_IsTrue(callback);\n            if (value >= 0) result = PyBool_FromLong(value);\n            break;\n        case 5:\n            value = PyObject_Length(callback);\n            if (value >= 0) result = PyLong_FromSsize_t(value);\n            break;\n        case 6: result = PyObject_GetIter(callback); break;\n        case 7: result = PyIter_Next(callback); break;\n        case 8: result = PyObject_GetItem(callback, other); break;\n        /* ... */\n        case 21:\n            result = PyType_Type.tp_call(callback, other, NULL);\n            break;\n        case 22: case 23: case 24: case 25: case 26:\n            result = conversion_probe(operation, callback); break;\n        case 27: case 28: case 29:\n            result = protocol_probe(operation, callback, other); break;\n        case 30: case 31: case 32: case 33: case 34: case 35:\n        case 36: case 37: case 38: case 39: case 40: case 41:\n        case 42: case 43: case 44: case 45: case 46:\n        case 47: case 48: case 49: case 50: case 51: case 52:\n        case 53: case 54: case 55: case 56: case 57: case 58: case 59:\n        case 60: case 61: case 62: case 63: case 64: case 65: case 66:\n        case 67: case 68: case 69: case 70: case 71: case 72:\n            result = collection_probe(operation, callback, other); break;\n        default: PyErr_SetString(PyExc_ValueError, \"bad probe operation\");\n    }\n```\n\nThis code style does not exist in the CPython code base, yet it shows up in newly generated code.\n\n``` php\nPyObject *info = PyTuple_Pack(3, name, mangled, suite->su_id);\nPyObject *flags = PyLong_FromLong(DEF_LOCAL);\nif (key == NULL || info == NULL || flags == NULL ||\n    PyDict_SetItem(suite->su_bindings, mangled, key) < 0 ||\n    PyDict_SetItem(st->st_cur->ste_block_bindings, key, info) < 0 ||\n    (private && PyDict_SetItem(st->st_binding_info, key, info) < 0) ||\n    (private && PyDict_SetItem(st->st_cur->ste_symbols, key, flags) < 0)) {\n    Py_DECREF(mangled); Py_XDECREF(key); Py_XDECREF(info); Py_XDECREF(flags);\n    goto error;\n}\nPy_DECREF(mangled); Py_DECREF(key); Py_DECREF(info); Py_DECREF(flags);\n```\n\nAs with the numbers for the operators, it also uses random integers in a list to stash away state.\n\n``` python\ndef _register_task(task):\n    \"\"\"Register an asyncio Task scheduled to run on an event loop.\"\"\"\n    _scheduled_tasks.add(task)\n    if _task_accelerator is not None:\n        _task_accelerator[6](task)\n\ndef _register_eager_task(task):\n    \"\"\"Register an asyncio Task about to be eagerly executed.\"\"\"\n    _eager_tasks.add(task)\n    if _task_accelerator is not None:\n        _task_accelerator[8](task)\n\ndef _enter_task(loop, task):\n    if (_task_accelerator is not None and\n            _task_accelerator[5]() is loop and loop not in _current_tasks):\n        return _task_accelerator[1](loop, task)\n    # ...\n```\n\nThis is not the codebase’s coding style, and quite frankly it should not be anyone’s coding style. I do not understand what motivated the model to do this.\n\n```\nstatic int\napply_layout(tokenizeriterobject *it)\n{\n    PyObject *source = PyBytes_FromStringAndSize(it->tok->source.bytes, it->tok->source.len);\n    if (source == NULL) return -1;\n    PyObject *events = _PyPegen_tokenize_layout(PyBytes_AS_STRING(source), it->tok->filename);\n    Py_DECREF(source);\n    if (events == NULL) return -1;\n    PyObject *result = PyList_New(0);\n    if (result == NULL) { Py_DECREF(events); return -1; }\n    Py_ssize_t index = 0;\n    PyObject *first_pos = PyTuple_GET_ITEM(PyList_GET_ITEM(it->pending, 0), 2);\n    PyObject *last_pos = PyTuple_GET_ITEM(PyList_GET_ITEM(it->pending, PyList_GET_SIZE(it->pending)-1), 2);\n    PyObject *previous = NULL;\n    for (Py_ssize_t i = 0; i < PyList_GET_SIZE(events); i++) {\n        PyObject *event = PyList_GET_ITEM(events, i);\n        if (previous && PyObject_RichCompareBool(previous, event, Py_EQ) == 1) continue;\n        previous = event;\n        PyObject *token = layout_token(it, event);\n        if (token == NULL) goto error;\n        if (token == Py_None) { Py_DECREF(token); continue; }\n        PyObject *pos = PyTuple_GET_ITEM(token, 2);\n        if (PyObject_RichCompareBool(pos, first_pos, Py_LE) == 1 ||\n            PyObject_RichCompareBool(pos, last_pos, Py_GT) == 1) { Py_DECREF(token); continue; }\n        while (index < PyList_GET_SIZE(it->pending)) {\n            PyObject *old = PyList_GET_ITEM(it->pending, index);\n            int cmp = PyObject_RichCompareBool(PyTuple_GET_ITEM(old, 2), pos, Py_LT);\n            if (cmp < 0) { Py_DECREF(token); goto error; }\n            if (!cmp) break;\n            if (PyList_Append(result, old) < 0) { Py_DECREF(token); goto error; }\n            index++;\n        }\n        if (index < PyList_GET_SIZE(it->pending)) {\n            PyObject *old = PyList_GET_ITEM(it->pending, index);\n            long kind = PyLong_AsLong(PyTuple_GET_ITEM(old, 0));\n            if ((kind == NL || kind == NEWLINE || kind == INDENT || kind == DEDENT) &&\n                PyObject_RichCompareBool(PyTuple_GET_ITEM(old, 2), pos, Py_EQ) == 1) index++;\n        }\n        if (PyList_Append(result, token) < 0) { Py_DECREF(token); goto error; }\n        Py_DECREF(token);\n    }\n    for (; index < PyList_GET_SIZE(it->pending); index++) {\n        if (PyList_Append(result, PyList_GET_ITEM(it->pending, index)) < 0) goto error;\n    }\n    Py_SETREF(it->pending, result);\n    Py_DECREF(events);\n    return 0;\nerror:\n    Py_DECREF(events);\n    Py_DECREF(result);\n    return -1;\n}\n```\n\nThe failure case here seems somewhat obvious: the model is trained for token efficiency for tool calling which also looks like code, and sometimes it seems to be taking that code into a place where it should not be: the codebase.\n\nI’m not really sure what to say here, but the slop machine was running for 35 hours until I turned it off. In that time it produced a net addition of 75k lines of code and it did not stop. In the 35 hours it burned around 1B tokens for a total of around 1200 USD in raw API costs. It managed to produce 79 commits, and that comes to a cost of around 15.5 USD per commit, and the agents exchanged around 1400 messages.\n\nI honestly do not need an agent to run for 35 hours on a single prompt. It clearly does not work or result in reasonable outputs.\n\nSo obviously: prompting it like this is stupid.  But when left unattended, it\n*will* keep going, and earlier models did not do that.  Even Fable wasn’t as\ncrazy as that.  When you accidentally give it slightly too big of a task, it will\ncontinue until it succeeds, even if it burns through an entire subscription.\n\nAnd that’s more or less why right now I do not manage to trust this model much. It has shown that it will commit slop, and it requires me to review it more as a result. Even if the failure rate is quite low, I would not want this.\n\nIn a world where code for tool calls is optimized for token efficiency and “getting the job done”, I wonder if there is really enough signal going to the training processes for “a human understands what is going on”. I would say that quite a lot of the code I get out of Astra is in my mind “objectively bad”. But it’s objectively bad by my human sense. Maybe it’s objectively good for a codebase that is entirely written by agents and only needs to be understood by agents.\n\nWhich is why I’m honestly asking myself more and more why we are doing this. These new models are absolutely amazing, for sure. But I’m more and more skeptical that the trajectory they are on still lends itself to present-day software engineering processes. The reason why I’m asking why we are doing this is because I felt like we achieved a pretty good spot for software engineering with those models, and that is the part of the AI economy where it was possible to show a positive return. But for how much more Fable costs, for how much more Astra costs, I do not feel like the results are there.\n\nIn fact, with Astra and Fable I feel like not only are the costs astronomical, but the models are also just not for me as a software engineer. And presumably that’s because these models increasingly are for other people. For lawyers, 3D artists, mathematicians, whoever uses computer use, etc.\n\nAnd potentially as a byproduct of enabling all of this, you can now slop your way to a one-shot 3D game over the weekend which looks impressive. And probably you can now run a software factory for as long as you don’t care about the code.\n\nI’m sure I will get used to this, but man this stuff is weird.\n\n**Postscriptum:** speaking of weird: how is it that these models, in a sandbox,\nwith supposedly no way to communicate with other agents, manage to find the [same\npublic wikis](https://collusion.wiki/) as a scratch pad for agent communication?\nDid they collude during training runs to remember resources on the internet\nwhich might come in handy in the future?\n\nI should clarify that I have done experiments like this before.  Typically\nthey do not run this long and the agent leaves behind a maybe imperfect but\nstill digestible piece of software.[↩](#fnref-1)", "url": "https://wpnews.pro/news/astra-for-coding-why-are-we-doing-this-again", "canonical_source": "https://lucumr.pocoo.org/2026/9/7/astra-why/", "published_at": "2026-09-07 00:00:00+00:00", "updated_at": "2026-09-09 17:56:29.256242+00:00", "lang": "en", "topics": ["large-language-models", "ai-research", "ai-products"], "entities": ["OpenAI", "GPT-6 Astra", "Armin Ronacher", "Codex", "Python"], "alternates": {"html": "https://wpnews.pro/news/astra-for-coding-why-are-we-doing-this-again", "markdown": "https://wpnews.pro/news/astra-for-coding-why-are-we-doing-this-again.md", "text": "https://wpnews.pro/news/astra-for-coding-why-are-we-doing-this-again.txt", "jsonld": "https://wpnews.pro/news/astra-for-coding-why-are-we-doing-this-again.jsonld"}}