๐ When AI Becomes a Library: The Nolife Local Experiment
on August 23, 2026
Most of the AI development I see still orbits the same center of gravity: ever larger general-purpose language models. That reflex is understandable. A model that can write poetry, reason about contracts, generate PHP, summarize documents, and answer questions feels like a universal tool. When every problem looks vaguely โAI-shaped,โ the temptation is to send it to the same endpoint.
But many application problems do not need a model capable of all of that. They need one model that performs one computational task extremely well.
Nolife Local is an experiment around that alternative. Instead of sending an image to a general-purpose multimodal LLM, I integrated a specialized vision model directly into a Symfony application and made it part of the applicationโs normal execution pipeline. The concrete case study is Depth Anything 3: monocular depth estimation, running locally on Apple Silicon through PHP FFI, orchestrated with Darkwood Flow, and built with a coding agent in a loop that slowly changed shape as the project grew.
Early on, that loop looked familiar:
idea โ ask agent to implement โ inspect result
By the end it looked more like:
observe โ collect evidence โ formulate problem โ define constraints
โ let agent investigate โ implement โ measure โ validate
The second loop is slower to start and much more reliable. The article follows both the runtime architecture and that shift in how the work itself was defined.
This is not an argument that LLMs are bad, that local AI is always better, or that small models replace general ones. The stronger conclusion is simpler:
The task should determine the model and the architecture.
General-purpose models remain excellent when the problem requires broad semantic reasoning. Specialized models become interesting when the desired output is itself specialized computation.
Not every AI problem is an LLM problem
The default developer reflex in 2026 is familiar:
problem โ large model โ prompt โ parse text
It works often enough that we stop asking whether it is the right abstraction. For many tasks it is. For others, it is like using a spreadsheet to edit pixels: possible in theory, expensive in practice, and the output shape is wrong.
What I wanted to see was the opposite shape:
problem โ specialized model โ dense numerical result โ application code
Depth estimation is a good stress test for that idea. The desired product is not a sentence about space. It is a field of numbers โ one value per pixel โ that can become a colormap, a camera estimate, or a 3D reconstruction. Language is a poor carrier for that result. A model trained to emit depth is a better one.
That distinction matters before any talk of PHP, FFI, or Symfony. If the computation itself is specialized, the model should be specialized too. The same instinct would later apply to how I used the coding agent: broad prompts produce broad (and often wrong) work. Narrow problems produce narrower, inspectable changes.
Depth from one image
Monocular depth estimation answers a deceptively simple question: given a single RGB photograph, can we estimate how far things are from the camera?
single RGB image
โ
Depth Anything 3
โ
dense geometric estimation
Humans do this constantly. A photograph of mountains already feels three-dimensional. Software has to recover that structure without stereo pairs, LiDAR, or a second viewpoint. The model must reason about occlusion, texture gradients, perspective, and object boundaries โ but the result is numeric, not narrative.
Depth Anything 3 (ByteDance) is a family of ViT-based models with a DualDPT head. The LocalAI teamโs depth-anything.cpp port runs them locally through ggml. No Python at inference time. No PyTorch runtime. No cloud vision API.
In Nolife Local we use DA3-BASE. Through the native C API, one dense inference returns:
- a depth map (
width ร heightfloats) - a confidence map (same resolution, when present)
- camera extrinsics (3ร4)
- camera intrinsics (3ร3)
- a flag indicating whether the depth is metric
For DA3-BASE, that flag is false. The depth values are relative, not meters. On our reference photograph the observed range was roughly 0.43โ3.47 in relative units. Treating those numbers as distances in meters would be a factual error. Metric depth exists in other DA3 variants (nested / mono paths); this demo does not use them.
From those outputs the native library can also build a glTF/GLB point cloud by back-projecting valid pixels into world coordinates. That is how the Symfony demo ends up with a 3D viewer, not by inventing geometry in PHP.
The research-to-runtime path looks like this:
ByteDance PyTorch checkpoint
โ convert once to GGUF (Python, offline)
GGUF weights on disk
โ ModelLoader (C++)
ggml compute graph
โ Metal / CPU / other backends
depth + confidence + camera
โ optional GLB export
application artifacts
We did not train Depth Anything 3. We did not fine-tune it, apply LoRA, or distill a student network. Specialization here means choosing an existing purpose-trained model and integrating it into a specialized local execution pipeline. Quantization (q4_k versus f32) is a separate dimension: a runtime/model-representation choice, not training.
Before writing PHP, the first work was understanding what the native stack actually did โ which C API functions existed, what memory contracts they implied, and what outputs were real rather than assumed from model names. That investigation became the first โissueโ in the project: understand inference before binding it. Skipping that step would have been the agent equivalent of choosing a general model because it sounds capable.
Specialized model versus general multimodal model
The contrast is not merely about size.
General multimodal path:
image
โ
large general model
โ
semantic interpretation
โ
text / structured answer
Specialized depth path:
image
โ
DA3
โ
dense numerical representation
โ
depth / confidence / camera / geometry
A multimodal LLM can discuss spatial relationships in language. That is a real capability, and for some products it is exactly what you want. It is not the same as emitting a deterministic 504ร280 float field, offline, from a ~99 MB GGUF, with structured camera parameters and a GLB export path.
The difference is the nature of the computation. Once the output is a dense geometric estimate, the model stops looking like a chatbot and starts looking like an application primitive: something you call, measure, cache, and compose.
That is the architectural shift Nolife Local explores at runtime. A parallel shift would appear in development: stop asking for general improvement; ask for the specific computation or integration step the application actually needs.
The unusual part: PHP
Then comes the deliberately awkward constraint.
The goal was not:
Call a Python microservice from Symfony.
Nor:
Send the image to an AI API.
The experiment asked:
How far can we integrate the native model directly into a Symfony application?
That leads to PHP FFI.
PHP does not execute the neural network kernels itself. Claiming otherwise would be marketing, not engineering. The ownership boundary in the final application is approximately:
Browser
โ
Symfony
โ
Darkwood Flow
โ
NativeDepthBridge
โ
PHP FFI
โ
libdepthanything
โ
ggml / Metal
โ
Depth Anything 3
Native results then return to PHP for application-level work.
PHP owns HTTP and upload handling, validation, orchestration, Flow step timing, native lifecycle coordination, result representation, GD colormap rendering, Twig / Symfony UX, CLI tooling, observability surfaces, and artifact management under var/inference/.
Native code owns preprocessing (resize, normalize, patch alignment), tensor operations, the ViT backbone, DualDPT depth/confidence heads, camera estimation, Metal execution, and native GLB reconstruction.
Symfony remains a web framework. Depth Anything remains a native vision engine. FFI is the seam between them.
Several tempting directions were deliberately not taken: pure PHP tensor inference, a Messenger worker farm, DDD layering, CQRS, or โwhile weโre hereโ Linux porting. None of those solved the current problem. They would have expanded scope because an agent could generate them, not because the experiment required them. Scope, I learned, has to be chosen by the human and written down before the agent starts typing.
PHP FFI as the boundary
FFI matters because it lets PHP own the lifecycle of a native context instead of shelling out to a CLI and hoping for the best.
The first useful question was not how much of the C++ code I could rewrite in PHP. It was where the useful boundary actually was. The brief for the FFI bridge was narrow on purpose:
Use da_capi_depth_dense, not a CLI wrapper.
Honor da_capi_free_floats for native buffers.
Respect that da_ctx is not thread-safe.
Produce an observable CLI or HTTP result.
That is already an issue in miniature: problem, constraints, expected outcome.
NativeDepthBridge loads libdepthanything.dylib, binds the C API from config/da_capi.h, and exposes a narrow surface to the rest of the application: load context, run dense inference, export GLB from cache, free buffers.
A focused excerpt of the boundary looks like this:
$rc = $ffi->da_capi_depth_dense(
$this->ctx,
$imagePath,
FFI::addr($outH),
FFI::addr($outW),
FFI::addr($outDepthPtr),
FFI::addr($outConfPtr),
FFI::addr($outSkyPtr),
$ext,
$intr,
FFI::addr($isMetric),
);
$depthPtr = $this->ffi->cast('float*', $outDepthPtr);
$depth = $this->copyFloatsFromPtr($depthPtr, $count);
The pattern is deliberately boring:
PHP
โ native function
โ native result pointer
โ copy into PHP arrays
โ da_capi_free_floats
Resource lifetime is part of the story. The bridge keeps a da_ctx while the model path stays unchanged, frees it on destruction or model swap, and serializes access with a file lock because the native context is not thread-safe. Float buffers returned by the C API are copied into PHP arrays and then freed immediately.
The brief was incomplete. During implementation, casting a large float* to float[141120] segfaulted on PHP 8.5. Indexed reads ($ptr[$i]) worked. That finding had to be added to the working context โ observed behavior over a plausible explanation. Agents are very good at sounding confident about memory layout. Reproduction won.
Later we stress-tested that lifetime inside one PHP process:
php -d ffi.enable=1 bin/console app:depth-stress \
-i public/samples/mountains.jpg \
-r 10
Ten full pipeline runs in-process kept GLB export on the cached path (~20 ms each) and did not require reloading the model between iterations. That is the kind of validation that turns โFFI works onceโ into โFFI works as application infrastructure.โ
Making it a Symfony feature
Once the bridge exists, the native model becomes an ordinary Symfony feature rather than a mysterious external service.
The user flow is:
upload image
โ
validate
โ
run local inference
โ
extract depth / confidence / camera
โ
generate visualization (PHP GD turbo colormap)
โ
reuse native result for GLB
โ
build result
โ
render Symfony UX
The upload page is intentionally small: drop an image, choose q4_k or f32, submit. While the request runs, the UI shows a processing state. A few seconds later the result page shows the original photograph beside the depth colormap, a confidence panel, camera summaries, pipeline timings, and a <model-viewer> for the GLB.
No custom WebGL engine. No Messenger workers. No Python sidecar. The Symfony application hosts the interaction; the native library performs the expensive computation; the browser only visualizes.
CLI tooling follows the same idea. Diagnostics treat the model like any other dependency:
FFI OK
GD OK
Native library OK
q4_k model OK
f32 model OK
Inference dir OK
READY
That output comes from php bin/console app:depth-check. The model is no longer an opaque AI endpoint. It is application infrastructure with a readiness check.
CLI inference prints the same pipeline story the web UI shows:
OK id=โฆ 504x336 infer=3179.4ms โฆ
validate: 3.49ms
infer: 3180.28ms
colormap: 75.52ms
glb: 19.65ms
result: 0.00ms
The important line for readers of benchmarks is that infer= is the Flow infer step, not a hidden synonym for โwhole request.โ Once that vocabulary is shared between CLI, web, and agent briefs, arguments about latency become much less mystical.
Orchestrating inference with Darkwood Flow
After the first working version, orchestration lived inside one service method: validate, infer, colormap, export, assemble. The pipeline was real, but implicit. Timings were either missing or collapsed into a single number that mixed very different kinds of work.
I introduced Darkwood Flow not as product placement, but because an expensive local model needs the same engineering properties as any other expensive computation: explicit steps, timings, failure handling, and a place to look when something feels wrong.
The final pipeline is:
Validate
โ
Infer
โ
Colormap
โ
Export scene
โ
Build result
Flow sits above the FFI bridge. NativeDepthBridge remains a narrow primitive. Flow owns sequencing and per-step wall-clock measurements.
Making the pipeline explicit turned out to matter more than making it clever. The integration iteration started with a small, well-scoped observation โ orchestration was monolithic โ and a concrete desired outcome: per-step timings in CLI and, later, in the web UI. That is issue-shaped work even before anyone names the pattern.
Flow did not make inference faster. It made the expensive steps visible. Visibility is a prerequisite for honest problem statements.
The benchmark that did not make sense
Early on, Symfony inference looked roughly like this:
~2.6 seconds
That matched the warm native numbers closely enough that I trusted it. The demo felt โabout three seconds.โ Then Flow made every step visible, and the arithmetic stopped adding up.
A warm run after Flow integration looked more like:
| Step | ms |
|---|---|
| Validate | ~3 |
| InferDepth | ~2639 |
| Colormap | ~86 |
| ExportScene | ~2535 |
| Total | ~5260 |
The InferDepth step alone still looked like the earlier ~2.6 s story. The full request the user waited for was closer to five seconds.
ExportScene was not โwriting a GLB.โ It was paying for another native inference.
At that moment, the valuable contribution was no longer โplease optimize the PHP code.โ It was a precisely formulated engineering problem:
Observation:
The complete pipeline takes much longer than infer-only.
Evidence:
Flow timings show another expensive native operation during GLB export.
InferDepth โ 2.6 s. ExportScene โ 2.5 s.
Constraint:
Preserve depth, confidence, camera, and GLB output.
Desired result:
One native inference per request.
Non-goal:
Do not solve this by adding concurrency or extra architecture layers.
Compare that to an early prompt like โimprove performance.โ The second brief is narrower and far more powerful. The agent does not need more freedom. It needs better context.
Finding the second inference
The call path before the fix was:
InferDepth
โ da_capi_depth_dense()
โ full ViT + DPT inference
ExportScene
โ da_capi_export_glb()
โ full ViT + DPT inference again
โ write GLB
da_capi_depth_dense() and da_capi_export_glb() were independent entry points. The export path prepared depth, confidence, pose, and RGB by running depth_pose_native() again. There was no cache between them. From the applicationโs point of view we already โhadโ the depth result; from the native APIโs point of view, GLB export did not know that.
This is the kind of bug that looks like a performance problem and is actually an architecture problem. Without step timings, the second pass hides inside a single user-facing wait. With step timings, it becomes obvious:
InferDepth โ 2.6 s
ExportScene โ 2.5 s
Two almost equal expensive steps are rarely a coincidence.
The investigation followed evidence, not speculation. I did not start by blaming Metal, quantization, PHP overhead, or memory bandwidth. I followed the timings to the call path, then read the native export preparation code. That discipline mattered because a coding agent can generate equally plausible wrong explanations at high speed. Observed behavior, logs, timings, code paths, and reproduction should outrank narrative.
One inference, several outputs
The fix was not asynchronous workers, Messenger, or a broader rewrite. It was reuse.
Conceptually:
BEFORE
image
โโโ infer โ depth
โโโ infer again โ GLB
AFTER
image
โ
one inference
โ
native result
โโโ depth
โโโ confidence
โโโ camera
โโโ GLB
On the native side this became ABI v11: da_capi_export_glb_cached(). Dense inference populates an export cache on the context; GLB writing consumes that cache without a second depth_pose_native(). On the PHP side, ExportScene calls exportGlbCached().
The decisive signal was the GLB step, not a contested millisecond on InferDepth.
Before (warm Flow run that exposed the bug):
| Step | ms |
|---|---|
| Validate | ~3 |
| InferDepth | ~2639 |
| Colormap | ~86 |
| ExportScene | ~2535 |
| Total | ~5260 |
After (experiments/004-pipeline-single-pass, mountains.jpg โ 504ร336, q4_k, warm, Apple M4 / Metal):
| Step | Median (ms) |
|---|---|
| Validate | ~3 |
| InferDepth | ~3165 |
| Colormap | ~88 |
| ExportScene (cached) | ~19 |
| Total | ~3274 |
Native inferences per request: 2 โ 1. ExportScene collapsed from roughly another full inference to a ~20 ms write from cache. InferDepth is higher in the โafterโ table because that measurement uses the larger demo-sample tensor (504ร336); the optimization is the deleted second pass, not a claim that InferDepth itself got faster.
The resulting code change was relatively small. Identifying exactly which work was duplicated was the valuable part. Observability found the duplicated work. The performance improvement came from deleting it.
That lesson is sharper than โadd caching somewhere.โ The expensive computation already produced every tensor the GLB exporter needed. The architecture just failed to notice.
What the measurements actually say
All numbers below were measured on Apple M4, macOS arm64, Metal. They are not universal Depth Anything 3 benchmarks. Different processed tensor sizes mean different workloads.
Measurement before optimization became a recurring rule. Not โPHP is slow,โ but: native infer, Flow/PHP steps, GLB export, full pipeline โ each with its boundary named.
Resolution is part of the benchmark
DA3 resizes so the longest side is about 504 pixels, with dimensions aligned to patch size 14. Source aspect ratio therefore changes the processed tensor:
| Source | Processed | Warm infer-only median (q4_k) |
|---|---|---|
| 1280ร720 reference photo | 504ร280 | ~2495 ms |
| 1024ร680 demo samples | 504ร336 | ~3.0โ3.2 s |
Those figures are not contradictory. 504ร336 has roughly 20% more pixels than 504ร280. Comparing them without naming the processed resolution is how fake regressions are invented.
When one run looked like ~2495 ms and another like ~3175 ms, the right next step was not another optimization prompt. It was a better problem statement: same hardware, same variant, different processed dimensions. Once the numbers disagreed, investigation beat speculation.
Warm infer-only at 504ร280
From experiments/003-warm-q4k-f32 (app:depth-bench, warmup 1, repeat 10, same PHP process):
| Variant | Median (ms) | Model size |
|---|---|---|
| q4_k | 2494.9 | ~99 MB |
| f32 | 2495.1 | ~393 MB |
Cold start is a different phenomenon
The first q4_k load in a process measured about 6.9 seconds in the reference CLI experiment, including Metal shader compilation. Warm loads afterward are tens or low hundreds of milliseconds. Mixing cold start into a โlatencyโ table without labeling it produces nonsense.
Flow / PHP overhead
Using same-run step timings on a warm full pipeline (experiments/005-flow-overhead, mountains.jpg):
validate ~3.5 ms
colormap ~90 ms
glb ~20 ms (cached)
result ~0 ms
Non-native Flow/PHP steps summed to roughly 114 ms. In this experiment, the orchestration side accounted for about 0.1 seconds of the request. Neural inference dominated.
Flow did not make Depth Anything faster. It made the double-pass bug discoverable, and afterward it made the remaining overhead honest.
CLI reference (native da3-cli, 504ร280)
From experiments/001-reference-cpp:
| Variant | Model size | Load | Infer median | Peak RSS |
|---|---|---|---|---|
| q4_k | ~99 MB | ~6869 ms cold* | 2589 ms | ~457 MB |
| f32 | ~393 MB | ~194 ms warm | 2580 ms | ~1028 MB |
* Cold q4_k load includes first-run Metal library init.
q4_k versus f32
Quantization is often sold as a free speedup. This experiment did not support that story on this hardware.
The question was not โwhich should feel faster?โ It was a controlled comparison: same process, same input, warmup then ten repeats, processed resolution recorded. Issue 009 captured that brief before implementation.
At 504ร280 warm inference, q4_k and f32 medians were effectively identical (~2495 ms). What changed dramatically was storage and memory footprint: roughly 99 MB versus 393 MB on disk, and much higher peak RSS for f32 in the CLI reference run.
The supported conclusion is therefore not โq4_k is four times faster.โ It wasnโt.
The supported conclusion is closer to:
Quantization dramatically reduced model storage in this experiment without producing a corresponding latency improvement on this particular Apple M4 / Metal workload.
I did not claim an accuracy winner. Without ground-truth depth for the demo photos, a โwhich is betterโ metric would be theater. The practical choice for the demo default is q4_k because the file is smaller and the warm latency matched f32 here. A depth-delta command without ground truth was explicitly rejected from scope for the same reason.
What โlocalโ means
Local inference and a fully offline application are related claims, not identical ones.
After install, Nolife Local performs no remote AI API call. Inference is FFI โ libdepthanything โ GGUF on disk โ Metal. That part was true early.
The stricter demo-runtime claim took more work. The result page originally loaded Googleโs <model-viewer> from a CDN while inference stayed local. That is a credibility trap: the AI is local, the page is not. We vendored public/vendor/model-viewer.min.js, removed the CDN reference from the demo path, and verified with HTTP checks and HTML inspection โ observed behavior again, not assumption.
Verified after that change:
| Claim | Result |
|---|---|
| Network required for inference | No |
| Network required for demo runtime (result page) | No |
Setup still needs the network once: Composer packages, model download, native build. FrankenPHP hot-reload CDN scripts appear only when that optional env flag is set; the publication demo uses the built-in PHP server without it.
So the honest phrase is not โAI in the cloud.โ It is also not โthe entire universe is offline forever.โ It is:
After install, this demo can run with networking disabled. The model is a local library.
AI models as application infrastructure
Once a specialized model behaves like a library, the surrounding tooling starts to look familiar to any Symfony developer.
php bin/console app:depth-check
php -d ffi.enable=1 bin/console app:depth-infer -i public/samples/mountains.jpg -m q4_k
php -d ffi.enable=1 bin/console app:depth-bench -i public/samples/mountains.jpg -w 1 -r 5 --json
php -d ffi.enable=1 bin/console app:depth-stress -i public/samples/mountains.jpg -r 10
php bin/console app:depth-cleanup --keep=20
Warmup exists for demos. Cleanup exists because every run creates artifacts. Stress exists because native cache lifetime across repeated in-process calls is part of the contract. Metadata is written beside the images. Pipeline totals are computed once in BuildResult, not re-summed differently in Twig and CLI.
The surprising feeling, after enough of this, is that Depth Anything stops reading as โan AI featureโ and starts reading as โa native dependency with diagnostics.โ
That is the point.
Context as an engineering artifact
By this point I had stopped treating the coding agent as something I asked to โimprove the project.โ Each iteration started with a smaller observed problem, evidence, constraints, and a success condition. Implementation became the last step rather than the first.
That is what I mean by issue-first development โ not project-management theater, but a compressed brief the agent can actually use.
An issue can become a compressed representation of everything the agent needs to understand about a problem. Not just โsomething is broken,โ but:
what happened
where it happened
environment
evidence
why it matters
constraints
expected behavior
what not to change
For Nolife Local, that context eventually included source code, the native API surface, model variant, processed resolution, Flow timings, benchmark methodology, error output, git history, known limitations, expected results, and explicit non-goals. The combination worked much better than a vague implementation request.
The projectโs issue files mirror that shape. Issue 007 for the double inference. Issue 009 for q4_k versus f32. Issue 003 for the FFI bridge, updated when PHP 8.5 disagreed with the first memory assumption. ITERATIONS.md records the same loop in chronological form: observation, problem selected, change, validation, measurement, next candidate.
observe โ issue โ agent โ implementation โ measure โ observe
Issue-first does not mean handing ownership to the agent.
Human: chooses the problem, defines intent, sets constraints, decides scope, evaluates trade-offs, validates results.
Agent: inspects, implements, measures, documents, iterates within the brief.
The agent can generate large amounts of code. That does not make it responsible for deciding what the project should become. A submitted patch can still contain insight, architecture understanding, and API design experience. Issue-first does not make code worthless. It makes problem understanding relatively more valuable when implementation is cheap to produce.
Specialize the model, specialize the context
There is a parallel here, and I treat it as an interpretation from the project rather than a universal law.
For computation:
general model
โ
specialized task model
โ
more constrained computation
For development:
general prompt
โ
well-defined issue
โ
more constrained implementation
The model side says: do not use the most general model when the task is already well defined. The agent side says: do not give the most general prompt when the engineering problem can be precisely defined. In both cases, reducing the search space can improve the result.
Specialize the model when the computation is specialized. Specialize the context when an agent is about to implement the next change.
What the experiment taught me
A few surprises, synthesized rather than re-tabled:
PHP FFI was sufficient for the integration boundary. The expensive part was not PHP orchestration. Observability exposed duplicate inference more effectively than intuition did. Processed resolution explained apparently inconsistent benchmarks. q4_k saved substantial disk space without an obvious warm-latency win on this Metal workload. And after enough tooling, a vision model can feel more like a native library than an AI service.
On the process side: implementation got cheaper; understanding the real problem did not. Configuration, environment, logs, measurements, and reproduction mattered as much as code. Agent-generated speculation was never equivalent to observed evidence.
Darkwood Flowโs strongest claim in this project is not that every Symfony application needs Flow. It is that once AI models become normal application components, they need the same properties as any expensive computation: orchestration, timings, failure handling, lifecycle management, and observability. In Nolife Local, that visibility revealed duplicated native work. That is concrete evidence, not a slogan.
Limitations
This experiment has a defined scope.
It was validated on macOS arm64 / Metal only. There is no Linux verification here, no Windows verification, and no production PHP-FPM deployment benchmark. We did not train or fine-tune Depth Anything 3. Performance numbers are hardware-specific. The demo implements one specialized model, not a multi-model production mesh. GLB quality was validated for the publication samples; absolute geometric accuracy against ground-truth depth was not the goal.
Those are not apologies. They are the edges of the claim โ chosen constraints, not missing ambition.
Conclusion
I set out to ask a practical question: instead of sending an image to a general-purpose multimodal LLM, what happens if a specialized vision model becomes part of a Symfony applicationโs normal pipeline?
What happened is that Depth Anything 3 behaved less like an โAI productโ and more like a library: load it, call it, free it, measure it, diagnose it, reuse its results. PHP did not become an ML framework. It orchestrated a native specialized component through FFI, made the pipeline observable with Flow, and turned a hidden double inference into a single-pass architecture.
The broader change may be this: as AI produces more implementation, engineering moves further toward choosing the right computation, defining the right problem, and validating the result.
Three layers stack in this experiment:
Depth Anything 3 โ specializes computation
Darkwood Flow โ makes computation observable and composable
Issue-first briefs โ specialize the context given to the coding agent
The conclusion is not โstop using LLMs.โ
It is closer to:
Stop assuming every AI-shaped problem should be solved by the same kind of model โ or the same kind of prompt.
A modern AI application can combine an LLM, a vision model, an embedding model, a speech model, a depth model, ordinary application code, and native libraries. Each should perform the work it is good at. Symfony does not need to reimplement ggml. It needs to be capable of hosting the boundary where those components meet.
Historically, open-source contribution often meant find a problem, write a patch, submit a pull request. Coding agents weaken the assumption that writing the patch is necessarily the scarce part. A future contribution can also be extremely valuable when someone provides precise reproduction, environment details, logs, configuration, use case, expected behavior, and constraints โ because those things allow a maintainer and an agent to implement the correct fix. Pull requests are not obsolete. Neither is a well-written issue. When implementation can be generated quickly, the interesting scarcity shifts toward intent and evidence.
Nolife Local is one version of that architecture:
Symfony + PHP + FFI + Darkwood Flow + specialized native model
with no remote inference API required after install.
Resources
Upstream and related projects:
-
Source code: https://github.com/matyo91/nolife-local
-
Slides: https://github.com/matyo91/slidewire