Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ env:
jobs:
framework:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: test
run: dotnet test DigitalBrain.slnx -c Release
run: dotnet test DigitalBrain.slnx -c Release --verbosity minimal
timeout-minutes: 12
21 changes: 11 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,33 +65,34 @@ the live system. Tests are the regression net, not the proof.
3. Drive the real scenario through the real edge — HTTP to `digitalbrain-ui`, or the Flutter shell.
4. **Read the journal** via `digitalbrain-mcp`: `read_neuron_journal`, `read_chat_transcript`,
`list_active_neurons`. Confirm the expected synapses fired, one correlation id, right order.
5. Cross-check Aspire — but only for what the table below says works.
5. Cross-check Aspire against structured logs and spans.
6. Root gate for regression.
7. Only now claim, quoting what you saw.

Journals are the audit source; telemetry is a projection and never replaces them. Journals hold
causal facts only — never arguments, prompts or secrets. Telemetry tags follow the same rule.
causal facts only — never arguments, prompts or secrets. Production telemetry follows the same rule;
the product AppHost opts Development into prompt and response capture for local diagnosis.

**What Aspire can actually tell you** (measured against a live AppHost after a real chat turn):

| Channel | State |
|---|---|
| `list_resources`, `list_console_logs` | works — console output is noisy with Azurite spam |
| `digitalbrain-mcp` journals | works, and is authoritative |
| `list_structured_logs` | **empty** |
| `list_traces` | **no application spans** — `dotnet-cli` only |
| GenAI spans, metrics | **do not exist** |
| Structured logs | works — MCP invocation completion includes tool name and error state |
| Application traces | works — ASP.NET, Orleans and kernel spans carry causal identifiers |
| GenAI spans, metrics | works — provider, model, duration, token usage and finish reason; prompt and response content only when the AI module explicitly enables it |

No host calls `ConfigureOpenTelemetry`, no chat client calls `UseOpenTelemetry`, and the kernel's
`ActivitySource("DigitalBrain")` has no exporter. **This is the top open defect.** Until it lands,
step 5 confirms health and console output only — say that, rather than implying traces were checked.
Update this table when you fix it.
`dotnet test os/tests/DigitalBrain.OS.Product.Tests -c Release -- -explicit only` is the live oracle.
It starts and stops the product AppHost, drives a real Gemma4 turn and retry, confirms the durable
transcript and correlation, checks owner-scoped active-neuron discovery, and verifies GenAI usage
and Development message content in the exported span.

## Gates

```powershell
dotnet build DigitalBrain.slnx -c Release
dotnet test DigitalBrain.slnx -c Release --logger "console;verbosity=minimal"
dotnet test DigitalBrain.slnx -c Release
```

**Never `--filter` for the completion gate** — a project-scoped run has already missed a failing
Expand Down
4 changes: 4 additions & 0 deletions DigitalBrain.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<Project Path="src/core/testing/DigitalBrain.Testing.Tests/DigitalBrain.Testing.Tests.csproj" />
<Project Path="src/core/aspire/DigitalBrain.Aspire/DigitalBrain.Aspire.csproj" />
<Project Path="src/core/aspire/DigitalBrain.Aspire.Hosting/DigitalBrain.Aspire.Hosting.csproj" />
<Project Path="src/core/aspire/DigitalBrain.ServiceDefaults/DigitalBrain.ServiceDefaults.csproj" />
<Project Path="src/core/aspire/DigitalBrain.ServiceDefaults.Tests/DigitalBrain.ServiceDefaults.Tests.csproj" />
<Project Path="src/core/kernel/DigitalBrain.SourceGeneration/DigitalBrain.SourceGeneration.csproj" />
<Project Path="src/core/security/DigitalBrain.Security/DigitalBrain.Security.csproj" />
<Project Path="src/core/mcp/DigitalBrain.Mcp/DigitalBrain.Mcp.csproj" />
Expand Down Expand Up @@ -54,6 +56,8 @@
<Project Path="os/tests/DigitalBrain.OS.Bdd.Tests/DigitalBrain.OS.Bdd.Tests.csproj" />
<Project Path="os/tests/DigitalBrain.OS.Composition.Tests/DigitalBrain.OS.Composition.Tests.csproj" />
<Project Path="os/tests/DigitalBrain.OS.Host.Tests/DigitalBrain.OS.Host.Tests.csproj" />
<Project Path="os/tests/DigitalBrain.OS.Mcp.Tests/DigitalBrain.OS.Mcp.Tests.csproj" />
<Project Path="os/tests/DigitalBrain.OS.Product.Tests/DigitalBrain.OS.Product.Tests.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/fixtures/DigitalBrain.Quickstart.Tests/DigitalBrain.Quickstart.Tests.csproj" />
Expand Down
14 changes: 11 additions & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.0.0-rc.1" />
<PackageVersion Include="ModelContextProtocol.Core" Version="2.0.0-rc.1" />
<PackageVersion Include="ModelContextProtocol.AspNetCore" Version="2.0.0-rc.2" />
<PackageVersion Include="ModelContextProtocol.Core" Version="2.0.0-rc.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.0-pre.5" />
<PackageVersion Include="xunit.v3" Version="4.0.0-pre.154" />
Expand All @@ -24,8 +24,16 @@
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.8.3" />
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.8.3" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.8.3" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.8.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.8.0" />
<PackageVersion Include="Microsoft.Agents.AI.Workflows" Version="1.15.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.30" />
<PackageVersion Include="OpenTelemetry.Api" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
<PackageVersion Include="Microsoft.Orleans.Client" Version="10.2.2" />
<PackageVersion Include="Microsoft.Orleans.Clustering.AzureStorage" Version="10.2.2" />
<PackageVersion Include="Microsoft.Orleans.Journaling" Version="10.2.2-rc.2.alpha.1" />
Expand All @@ -37,4 +45,4 @@
<PackageVersion Include="Microsoft.Orleans.Serialization.SystemTextJson" Version="10.2.2" />
<PackageVersion Include="Microsoft.Orleans.TestingHost" Version="10.2.2" />
</ItemGroup>
</Project>
</Project>
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ await brain.SendAsync<IAnalyst>(
```

The owner-bound `IDigitalBrain` facade enters through a session; neurons call typed capabilities such
as `ILlama32` inside the brain. The same vocabulary will later carry approved C# behaviors generated
as `IGemma4` inside the brain. The same vocabulary will later carry approved C# behaviors generated
from natural language.

## The shape of it

- **A synapse is a fact** — a thin record, broadcast, no reply. **An interface method is a request** —
directed at a capability, and it replies. Both are journaled; neither is privileged.
- **Modules own vocabulary** — synapse records and neuron interfaces, resolved at compile time.
- **Namespaces and type names are architecture** — `DigitalBrain.AI.Ollama.ILlama32` is identity, not
- **Namespaces and type names are architecture** — `DigitalBrain.AI.Ollama.IGemma4` is identity, not
a lookup result.
- **Journals are the audit source**, recording causal facts only — never arguments, prompts or
secrets. Telemetry is a projection and never replaces them.
Expand All @@ -37,11 +37,13 @@ The plan of record. Nothing is shipped unless it says Built.
| Neuron/synapse foundation, owner-scoped client, module activation, AppHost composition, testing path | **Built** |
| Typed AI, Tasks, Google, Salesforce, Chat, Flutter, Quickstart families | **Built** |
| Flutter vertical — shell/scene vocabulary, UI HTTP/SSE edge, `WithUIEdge`/`WithFlutterHost`, headless Dart host, Windows chrome | **Built** |
| Product shell — responsive Chat, content-safe Activity, live 3D-projected Brain topology, pulses and inspector | **Built** |
| Product MCP surface — durable chat send/read, neuron journal observation, active-neuron discovery | **Built** |
| Time — durable one-shot `ICountdown` and its recovery tests | **Built** |
| Time — reminders, recurring interval/calendar scheduling, DST | Designed |
| Product chrome polish, multi-principal IdP edge, journal observation on `IDigitalBrain` | Designed |
| Multi-principal IdP edge, journal observation on `IDigitalBrain` | Designed |
| Behavior rail — proposal, approval, installation, execution, rollback | Designed |
| Observability spine — host OpenTelemetry, instrumented chat clients, GenAI spans | **Not built — top open defect** |
| Observability spine — host OpenTelemetry, structured logs, causal kernel spans, GenAI spans and metrics | **Built** |

`DigitalBrain.Behaviors` is a packable SDK foundation (authoring interfaces, constrained context,
manifests, artifact identities) and holds the canonical artifact codec. It is not a compiler, builder,
Expand Down Expand Up @@ -70,6 +72,13 @@ git clean -fdx
aspire run
```

The explicit product suite performs a self-cleaning live Aspire proof across resource health,
Gemma4 chat, command retry, durable journals, owner-scoped introspection, and OpenTelemetry:

```powershell
dotnet test os/tests/DigitalBrain.OS.Product.Tests -c Release -- -explicit only
```

[CLAUDE.md](CLAUDE.md) is the working discipline for every agent and contributor: the gates, the
verification ladder, and the traps. A green test suite is necessary, not sufficient — it proves the
code holds, not that a behaviour works.
Expand Down
3 changes: 2 additions & 1 deletion clients/digitalbrain_flutter/bin/digitalbrain_host.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import 'package:digitalbrain_flutter/digitalbrain_flutter.dart';
Future<void> main(List<String> args) async {
final shell = _arg(args, '--shell') ?? DigitalBrainHostEnv.resolveShell();
final open = _arg(args, '--open');
final reconnectSeconds = int.tryParse(_arg(args, '--reconnect-seconds') ?? '') ?? 2;
final reconnectSeconds =
int.tryParse(_arg(args, '--reconnect-seconds') ?? '') ?? 2;

final client = DigitalBrainUiEdgeClient.fromEnvironment();
final surface = ShellSurfaceController();
Expand Down
18 changes: 11 additions & 7 deletions clients/digitalbrain_flutter/lib/digitalbrain_flutter.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export 'package:digitalbrain_wire/digitalbrain_wire.dart' show
ActivateControlRequest,
ChatTurnEvent,
OpenSceneRequest,
SceneOpenedEvent,
SendMessageRequest;
export 'package:digitalbrain_wire/digitalbrain_wire.dart'
show
ActivateControlRequest,
BrainModule,
BrainNeuron,
BrainTopologySnapshot,
ChatTurnEvent,
OpenSceneRequest,
SceneOpenedEvent,
SendMessageRequest;

export 'src/edge_client.dart';
export 'src/host_environment.dart';
export 'src/shell_surface.dart';
export 'src/sse_chat_frames.dart';
export 'src/sse_frames.dart';
export 'src/sse_frames.dart';
58 changes: 53 additions & 5 deletions clients/digitalbrain_flutter/lib/src/edge_client.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';

import 'package:digitalbrain_wire/digitalbrain_wire.dart';
Expand All @@ -8,11 +9,9 @@ import 'sse_chat_frames.dart';
import 'sse_frames.dart';

final class DigitalBrainUiEdgeClient {
DigitalBrainUiEdgeClient({
required this.baseUri,
http.Client? httpClient,
}) : _http = httpClient ?? http.Client(),
_ownsClient = httpClient == null;
DigitalBrainUiEdgeClient({required this.baseUri, http.Client? httpClient})
: _http = httpClient ?? http.Client(),
_ownsClient = httpClient == null;

factory DigitalBrainUiEdgeClient.fromEnvironment({
http.Client? httpClient,
Expand Down Expand Up @@ -148,6 +147,55 @@ final class DigitalBrainUiEdgeClient {
}
}

Future<BrainTopologySnapshot> readBrainTopology({
Duration requestTimeout = const Duration(seconds: 5),
}) async {
final uri = baseUri.replace(path: '/brain/topology');
final abort = Completer<void>();
final operation = () async {
final request = http.AbortableRequest(
'GET',
uri,
abortTrigger: abort.future,
);
final streamed = await _http.send(request);
return http.Response.fromStream(streamed);
}();
final response = await operation.timeout(
requestTimeout,
onTimeout: () {
abort.complete();
throw http.RequestAbortedException(uri);
},
);
if (response.statusCode != 200) {
throw StateError(
'brain-topology failed: ${response.statusCode} ${response.body}',
);
}

final decoded = jsonDecode(response.body);
if (decoded is! Map) {
throw const FormatException('brain-topology response is not an object');
}

return BrainTopologySnapshot.fromJson(Map<String, Object?>.from(decoded));
}

Stream<BrainTopologySnapshot> watchBrainTopology({
Duration pollInterval = const Duration(seconds: 2),
Duration requestTimeout = const Duration(seconds: 5),
}) async* {
while (true) {
try {
yield await readBrainTopology(requestTimeout: requestTimeout);
} on Object catch (error, stackTrace) {
yield* Stream<BrainTopologySnapshot>.error(error, stackTrace);
}
await Future<void>.delayed(pollInterval);
}
}

void close() {
if (_ownsClient) {
_http.close();
Expand Down
4 changes: 3 additions & 1 deletion clients/digitalbrain_flutter/lib/src/shell_surface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ final class ShellSurfaceController {
title: event.title,
sequence: event.sequence,
);
final index = _scenes.indexWhere((scene) => scene.sceneKey == view.sceneKey);
final index = _scenes.indexWhere(
(scene) => scene.sceneKey == view.sceneKey,
);
if (index >= 0) {
_scenes[index] = view;
} else {
Expand Down
4 changes: 1 addition & 3 deletions clients/digitalbrain_flutter/lib/src/sse_frames.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ final class SseSceneOpenedParser {
if (decoded is! Map) {
return null;
}
return SceneOpenedEvent.fromJson(
Map<String, Object?>.from(decoded),
);
return SceneOpenedEvent.fromJson(Map<String, Object?>.from(decoded));
} on FormatException {
return null;
} on TypeError {
Expand Down
Loading