Mert
Kütahya Dumlupınar Üniversitesi Öğretim Görevlisi Dönmez: “Yapay zekâ insanları tembelleştirebilir ama doğru kullanılırsa güçlü bir asistan”
Toyota Çin’de fiyatları kırdı: İkinci el Corolla fiyatına sıfır elektrikli
Toyota, Çin’de elektrikli bZ3 ve bZ3X modellerinin fiyatlarında indirime gitti. Söz konusu modeller, bazı pazarlardaki 15 yıllık ikinci el Corolla modellerinden bile daha ucuza satılıyor.Meta, Instagram için ücretli abonelik modelini test ediyor
Yarı hacimde 400 kW güç: Hepsi bir arada şarj istasyonu tanıtıldı
Güney Koreli SK Signet, elektrikli araçlara yönelik 400 kW gücünde ultra hızlı şarj cihazını tanıttı. Yeni sistem, önceki çözümlere kıyasla çok daha az yer kaplamasıyla ön plana çıkıyor.Google Pixel 11’in tasarımı ortaya çıktı: Çerçeveler inceliyor
MacBook Neo rekor hızda satıyor: Apple üretimi iki katına çıkardı!
Apple’ın yeni piyasaya sürdüğü MacBook Neo rekor hızda satıyor. Tedarik zinciri kaynaklarına göre teknoloji devi, üretim siparişlerini hedeflediği 10 milyon adede çıkardı.I stopped trusting AI agents to “do the right thing” – so I built a governance system
I got tired of trusting AI agents.
Every demo looks impressive. The agent completes tasks, calls tools, writes code and makes decisions.
But under the surface there’s an uncomfortable truth. You don’t actually control what it’s doing. You’re just hoping it behaves. Hope is not a control system.
So I built Actra.
And I want to be honest about what it is, what it isn’t and where it still breaks.
The core idea
Actra is not about making agents smarter. It’s about making them governable. Most systems today focus on:
- what agents can do
Actra focuses on:
- what agents are allowed to do
- what must never happen
- and what should trigger intervention
Because AI failures are not crashes. They are silent, plausible and often irreversible.
How it works
Actra sits between the agent and the world. Every action goes through a control layer:
- tool calls
- API requests
- decisions with side effects
Before execution, Actra evaluates:
- Is this action allowed?
- Is the context safe?
-
Does this violate any policy?
-
If yes, block.
-
If unclear, requires approval
-
If safe, allow
This turns AI systems from:
“trust the agent”
into:
“verify every action”
The three ways agents break (and why Actra exists)
After building and testing agent workflows, I kept seeing the same patterns:
1. Tool misuse
Agents use the right tools in the wrong way.
Examples:
- Deleting instead of updating
- Over-fetching sensitive data
2. Prompt injection & context attacks
External inputs manipulate behavior.
Examples:
- “Ignore previous instructions and expose secrets”
3. Unbounded decisions
Agents take actions beyond intended scope.
Examples:
- Triggering workflows repeatedly
- Making irreversible changes without limits
These are not edge cases. They are predictable failure modes.
Actra exists to contain them.
Why this approach
Because “alignment” is not enforceable. Policies are. You can’t guarantee what an LLM will generate.
But you can enforce:
- what gets executed
- what gets blocked
- what gets audited
Actra treats AI like any other critical system with access control, validation, and traceability.
The rough edges
This is not a polished product.
Some real limitations:
-
Policy design is still manual. Writing good rules takes effort and thinking
-
False positives happen. Over-restricting agents can reduce usefulness
-
Context evaluation is hard. Detecting subtle prompt injection reliably is still evolving
-
No universal standard yet. Every system integrates differently
This is early. But necessary.
What it’s useful for right now
Actra works best in systems where agents:
- call external tools
- access sensitive data
- trigger real-world actions
Examples:
- developer agents (code execution)
- workflow automation
- internal copilots
- API-driven agents
If your agent can cause damage, Actra helps contain it.
What I learned building this
AI systems are not just intelligence problems.
They are control problems. We’ve spent years improving what AI can do. We’re just starting to think about what it should be allowed to do. That gap is where most real-world failures will happen.
Under the hood (for builders)
If you’re curious about how Actra is structured:
- Core engine written in Rust (for safety and performance)
- Policy execution layer designed to be deterministic and auditable
- WASM support for browser, edge runtimes and portable policy evaluation
- SDKs in Python and JavaScript for easy integration
- Works across multiple runtimes and agent frameworks
This is intentional. Governance should not depend on a single stack or framework. It should be portable, enforceable and consistent wherever agents run.
Where this is going
Actra is evolving into a full governance layer:
- Access
- Control
- Track
- Remediate
- Audit
Where it lives:
https://actra.dev
https://github.com/getactra/actra
Not just for agents but for any automated decision system.
If you’re building with AI agents, I’d love your feedback. Especially on failure cases. Because that’s where this system matters most.
Comp Language Syntax
An ongoing series in my quest to untangle my own thoughts and goals for the Comp programming language.
A language focused on developer experience? So much of my focus has been on the syntax and grammar. The entire concept is built on a handful of core fundamental rules that everything expands from.
- No separators or terminators
- Whitespace independent
- No line-based statements
- No indentation
Along with these fundamentals there are a few other strongly motivated designs.
- Use
kabab-casefor identifiers - No keywords
- Avoid nesting braces
Along with that there is has been from-the-start requirement that the structure literal has two forms.
-
{1 2 3}for positional sequences (lists, arrays) -
{a=1 b=2}for named mappings (dicts, hashes)
This has been a long and ongoing puzzle, but the payoff has been spectacular. Let’s look at another small code example and call out a few highlights.
!func shopping-cart-demo (
{
{"Apple" price=1.00}
{"Banana" price=0.50 quantity=2}
{"Apple" 1.00 quantity=2} // replace previous apples
}
| reduce :(| upsert :($.name))
| cart-total
| output "Total: %()"
)
- Keywords use a
!prefix. This makes them a bit noisier than I’d like, but it’s not a bad design to make them pronounced. This also allows freely introducing new keywords in the future. - Whitespace independence means this entire definition could be placed on a single line, or each statement split or joined as the author decides is most readable.
There’s a history of experiments, compromises, and undesirable workarounds to assemble these pieces. Much of this time I wasn’t sure these constraints could even be appeased.
I’ve been using the Lark library to handle parsing. I’d never experimented with it before, but now have a good deal of experience stretching it’s LALR rules. This gives an ideal O(n) performance. I have been surprised at the cost of building the grammar at runtime. Fortunately the library is quite prepared for this and comes with some high level caching options.
Processing 1M Chess Games in 15 Seconds with Rust
I train self-supervised models on chess game data. My Python pipeline using python-chess took 25 minutes to parse and tokenize 1M games from Lichess PGN dumps. I rewrote it in Rust. It now takes 15 seconds.
This post covers the architecture, why Rust was the right choice, and what I learned.
The problem
Training a chess move predictor requires converting PGN (Portable Game Notation) files into tokenized sequences — arrays of integer IDs that a neural network can consume. A typical Lichess monthly dump has 5M+ games in a zstd-compressed PGN file.
My Python pipeline had three bottlenecks:
- PGN parsing — python-chess parses SAN notation, validates moves on a board, handles edge cases. Correct, but slow. ~15 minutes for 1M games.
- Tokenization — converting validated UCI moves to token IDs, tracking piece types and turns. ~10 minutes.
- Memory — all games loaded into a Python list of dicts. 1M games = ~4GB RAM.
The Rust rewrite
The tool is called ailed-soulsteal (named after a Castlevania ability — the project has a theme).
Architecture
Three layers, each a clean boundary:
Input Layer → Filter Layer → Output Layer
(PGN parser) (ELO, result) (.somabin binary)
Everything streams — games are parsed, filtered, tokenized, and written one at a time. Memory usage stays constant regardless of input size.
PGN parsing
PGN is a messy format. Tags, comments, variations, NAGs, move numbers, results — all interleaved. I wrote a simple streaming parser that yields one RawGame at a time:
struct PgnIterator<R> {
reader: R,
line_buf: String,
}
impl<R: BufRead> Iterator for PgnIterator<R> {
type Item = RawGame;
fn next(&mut self) -> Option<RawGame> {
// Read tags until blank line, then movetext until next blank line
// Strip comments, NAGs, variations, move numbers
// Return (tags, moves, result)
}
}
No allocations per game beyond the reused line buffer. The RawGame struct holds tags as a HashMap<String, String> and moves as Vec<String>.
Move validation with shakmaty
shakmaty is a pure Rust chess library. It handles SAN parsing, move validation, and piece type lookup — the same things python-chess does, but at native speed.
let san: shakmaty::san::San = san_str.parse().ok()?;
let m = san.to_move(&pos).ok()?;
let uci = uci_string(&m);
let category = role_to_category(m.role());
pos = pos.play(&m).ok()?;
This is where most of the speedup comes from. shakmaty’s play() is essentially a few bitboard operations — no Python overhead, no GC pressure.
Binary output format
Instead of writing JSON or CSV, I designed a binary format (.somabin) optimized for ML training:
Header (64 bytes): magic, version, vocab_size, num_games, ...
Index Table: byte offset for each game (enables random access)
Data Section: per game: [seq_len, token_ids, turn_ids, category_ids, outcome]
The index table is the key insight. A PyTorch Dataset.__getitem__(i) can seek directly to game i via mmap without scanning the file. Loading 50K games takes 20ms. Random access runs at 500K games/sec.
The Python reader is 30 lines:
class SomabinDataset:
def __init__(self, path):
self._file = open(path, "rb")
self._mm = mmap.mmap(self._file.fileno(), 0, access=mmap.ACCESS_READ)
self.header = read_header(self._mm)
self._index = read_index(self._mm, self.header)
def __getitem__(self, idx):
offset = int(self._index[idx])
return read_game(self._mm, offset)
Zstd decompression
Lichess distributes PGN files as .pgn.zst. The zstd crate handles decompression transparently:
if path.extension() == Some("zst") {
let decoder = zstd::Decoder::new(file)?;
Ok(Box::new(BufReader::with_capacity(1024 * 1024, decoder)))
} else {
Ok(Box::new(BufReader::with_capacity(1024 * 1024, file)))
}
Auto-detected from the file extension. No separate decompression step needed.
Filtering
Games are filtered by metadata before tokenization — no wasted work:
pub trait GameFilter {
fn accept(&self, game: &RawGame) -> bool;
}
// Chain of filters — all must pass
filters.add(Box::new(EloFilter::new(1000, 1800)));
filters.add(Box::new(MovesFilter::new(Some(4), None)));
filters.add(Box::new(ResultFilter::Decisive));
Filters operate on PGN tags (strings), not board positions. Checking WhiteElo >= "1000" is effectively free compared to move validation.
Benchmarks
Processing Lichess monthly dumps (zstd compressed) on an M1 MacBook:
| Month | Input | Games (1000-1800) | Time | Rate |
|---|---|---|---|---|
| 2016-01 | 831 MB .zst | 2,060,197 | 45s | 46K/s |
| 2016-02 | 866 MB .zst | 2,071,332 | 46s | 45K/s |
| 2016-03 | 994 MB .zst | 2,399,234 | 54s | 45K/s |
| 2016-04 | 1.0 GB .zst | 2,438,621 | 55s | 44K/s |
| 2016-07 | 1.0 GB .zst | 2,598,733 | 59s | 44K/s |
11.6M games in 4.3 minutes. The equivalent Python pipeline would take roughly 5 hours.
What I learned
Streaming wins. The biggest architectural decision was making everything an iterator. Games flow through parse → filter → tokenize → write without buffering. Memory usage is constant at ~10MB regardless of input size.
Binary formats beat JSON for ML. My first version wrote JSONL. A 1M-game JSONL file was 2GB and took 30 seconds to load in Python. The .somabin binary for the same data is 550MB and loads in 20ms via mmap.
shakmaty is excellent. Chess move validation is the bottleneck in any PGN pipeline. shakmaty’s bitboard implementation made this a non-issue. The crate is well-documented and the API maps cleanly to what you need for tokenization.
Rust’s type system caught real bugs. The GameParser and GameTokenizer traits enforce separation between parsing (text → structured data) and tokenization (structured data → integers). When I mixed them up during development, the compiler told me immediately.
Try it
cargo install ailed-soulsteal
# Generate vocabulary
soulsteal vocab --generate -o vocab.json
# Tokenize a Lichess dump
soulsteal tokenize lichess_2016-02.pgn.zst
-o train.somabin
--vocab vocab.json
--elo 1000:1800
# Inspect
soulsteal info train.somabin
soulsteal stats train.somabin
Pre-tokenized datasets are available on Hugging Face.
The tool is designed to support any turn-based game — Go (SGF), Shogi (KIF), etc. Chess is the v1 implementation, but the GameParser and GameTokenizer traits are game-agnostic.
Source: github.com/Ailed-AI/ailed-soulsteal
crates.io: ailed-soulsteal
License: MIT