Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
8285dac
llm-benchmark: add normalized table evals
bradleyshep Jul 17, 2026
e51e287
llm-benchmark: add behavioral table evals
bradleyshep Jul 17, 2026
ba223b3
llm-benchmark: add reducer behavior evals
bradleyshep Jul 17, 2026
0d3f74c
llm-benchmark: add scheduled batch deletion eval
bradleyshep Jul 17, 2026
fb2f938
llm-benchmark: add query builder view evals
bradleyshep Jul 17, 2026
644340e
llm-benchmark: add procedural view evals
bradleyshep Jul 17, 2026
2f647d6
llm-benchmark: complete view eval coverage
bradleyshep Jul 17, 2026
ba06e21
llm-benchmark: add auth and lifecycle evals
bradleyshep Jul 17, 2026
8c510ff
llm-benchmark: add procedure and http evals
bradleyshep Jul 17, 2026
ce87575
llm-benchmark: add migration-aware default eval
bradleyshep Jul 17, 2026
633f6d7
llm-benchmark: add migration and RLS evals
bradleyshep Jul 17, 2026
8106177
Support stable local LLM benchmark builds
bradleyshep Jul 17, 2026
6c3139f
Retry silent LLM benchmark publish failures
bradleyshep Jul 17, 2026
104fafd
Make local LLM benchmark logs collision proof
bradleyshep Jul 17, 2026
25c8b31
Include existing modules in migration eval prompts
bradleyshep Jul 17, 2026
401b206
Make advanced LLM evals self-contained
bradleyshep Jul 17, 2026
f49cc64
Document advanced TypeScript server APIs
bradleyshep Jul 17, 2026
43c0330
Validate LLM benchmark goldens behaviorally
bradleyshep Jul 17, 2026
6d06baa
Generalize advanced TypeScript server guidance
bradleyshep Jul 17, 2026
9a775e5
Clarify advanced view eval requirements
bradleyshep Jul 17, 2026
2f6bb29
Document TypeScript server API edge cases
bradleyshep Jul 17, 2026
b3f4bbc
Grade only specified HTTP response headers
bradleyshep Jul 17, 2026
4a084de
Bound LLM benchmark scorer calls
bradleyshep Jul 17, 2026
239ebbe
Document advanced Rust and C# server APIs
bradleyshep Jul 17, 2026
fd97271
Refine TypeScript eval guidance and prompts
bradleyshep Jul 17, 2026
87ee8c8
Align new eval prompts with benchmark format
bradleyshep Jul 17, 2026
5567d91
Retry transient HTTP eval calls
bradleyshep Jul 17, 2026
3c250b9
Clarify ambiguous eval task contracts
bradleyshep Jul 18, 2026
4a1252e
Fix reducer schema parity extraction
bradleyshep Jul 18, 2026
081432f
Strengthen view eval result grading
bradleyshep Jul 18, 2026
d39578f
Ignore reducer parameter names in schema parity
bradleyshep Jul 18, 2026
97ba8af
Clarify private table eval contracts
bradleyshep Jul 18, 2026
4af3acd
Clarify aggregate table visibility
bradleyshep Jul 18, 2026
7958c49
Allow complete benchmark module responses
bradleyshep Jul 18, 2026
2d00980
Avoid implicit response caps for optional providers
bradleyshep Jul 18, 2026
e040c8d
Clarify private schema and scheduled exports
bradleyshep Jul 18, 2026
ccf14de
Retry silent Rust compiler exits
bradleyshep Jul 18, 2026
d188c8f
Align Rust lifecycle prompt with golden schema
bradleyshep Jul 18, 2026
ef4b8b1
Retry diagnostic-free Cargo exits
bradleyshep Jul 18, 2026
9e7607f
Fix Rust external upload golden capture
bradleyshep Jul 18, 2026
c699171
Clarify Rust lifecycle and HTTP APIs
bradleyshep Jul 18, 2026
414e2b1
Generalize LLM eval guidance and strengthen checks
bradleyshep Jul 19, 2026
905249e
Clarify TypeScript eval guidance
bradleyshep Jul 19, 2026
3d21ee7
Document TypeScript module type contracts
bradleyshep Jul 19, 2026
f672791
Clarify Rust query and HTTP context contracts
bradleyshep Jul 20, 2026
5fb7784
Correct Rust procedural view guidance
bradleyshep Jul 20, 2026
d38f6d9
Correct Rust string accessor guidance
bradleyshep Jul 20, 2026
3dfba78
Clarify C# view and HTTP contracts
bradleyshep Jul 20, 2026
4b41dae
Clarify C# HTTP types and presence IDs
bradleyshep Jul 20, 2026
005a093
Retry transient dotnet linker failures
bradleyshep Jul 20, 2026
25e0b50
updates
bradleyshep Jul 20, 2026
62aa9e9
updates
bradleyshep Jul 20, 2026
5d63ebe
Update SKILL.md
bradleyshep Jul 20, 2026
5fe4011
Update StdbModule.csproj
bradleyshep Jul 20, 2026
35c5d42
Update templates.rs
bradleyshep Jul 21, 2026
6cb8317
Harden Rust LLM benchmark evaluation
bradleyshep Jul 21, 2026
6719f9d
fixes
bradleyshep Jul 21, 2026
b59d00d
Improve C# benchmark reliability
bradleyshep Jul 21, 2026
219d866
Clarify C# connection ID encoding eval
bradleyshep Jul 21, 2026
b58c1fa
Merge remote-tracking branch 'origin/bradley/fix-csharp-llm-bench' in…
bradleyshep Jul 21, 2026
e96a9fc
Harden LLM benchmark validation and cleanup
bradleyshep Jul 21, 2026
9bcdc72
Clarify HTTP APIs in server benchmark skills
bradleyshep Jul 22, 2026
5c5eb8f
Merge branch 'master' into bradley/new-llm-benchmark-evals-0717
bradleyshep Jul 22, 2026
438f71c
Clarify TypeScript benchmark guidance
bradleyshep Jul 22, 2026
5aac866
Harden benchmark regression tests
bradleyshep Jul 22, 2026
bdbeb96
Remove bias
bradleyshep Jul 22, 2026
21125cb
remove bias
bradleyshep Jul 22, 2026
a4a4a3a
cleanup
bradleyshep Jul 22, 2026
07ad955
Use workspace CLI for LLM benchmark runs
bradleyshep Jul 22, 2026
173fa66
Parallelize periodic benchmarks by language
bradleyshep Jul 23, 2026
02e1781
Merge branch 'master' into bradley/new-llm-benchmark-evals-0717
bradleyshep Jul 23, 2026
3483bd6
Keep parallel Rust benchmark builds isolated
bradleyshep Jul 23, 2026
30dbf5a
Refine TypeScript eval guidance and randomness scoring
bradleyshep Jul 23, 2026
3571338
updates
bradleyshep Jul 23, 2026
808baa4
Merge branch 'master' into bradley/new-llm-benchmark-evals-0717
bradleyshep Jul 23, 2026
5b46b13
lints
bradleyshep Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
97 changes: 49 additions & 48 deletions .github/workflows/llm-benchmark-periodic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,29 @@ concurrency:
cancel-in-progress: true

jobs:
prepare_matrix:
name: Prepare benchmark matrix
runs-on: ubuntu-latest
outputs:
languages: ${{ steps.matrix.outputs.languages }}
steps:
- id: matrix
env:
INPUT_LANGUAGES: ${{ inputs.languages || 'rust,csharp,typescript' }}
run: |
languages="$(jq -cn --arg value "$INPUT_LANGUAGES" '$value | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))')"
echo "languages=$languages" >> "$GITHUB_OUTPUT"

run-benchmarks:
name: Run benchmarks (${{ matrix.lang }})
needs: prepare_matrix
runs-on: spacetimedb-new-runner-2

strategy:
fail-fast: false
matrix:
lang: ${{ fromJSON(needs.prepare_matrix.outputs.languages) }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -61,11 +81,13 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Setup .NET SDK
if: matrix.lang == 'csharp'
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Install WASI workload
if: matrix.lang == 'csharp'
env:
DOTNET_MULTILEVEL_LOOKUP: "0"
DOTNET_CLI_HOME: ${{ runner.temp }}/dotnet-home
Expand All @@ -74,25 +96,25 @@ jobs:
dotnet workload install wasi-experimental --skip-manifest-update --disable-parallel

- name: Pack C# runtime packages
if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'csharp') }}
if: matrix.lang == 'csharp'
run: |
dotnet pack -c Release crates/bindings-csharp/BSATN.Runtime
dotnet pack -c Release crates/bindings-csharp/Runtime

- name: Set up Node.js
if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }}
if: matrix.lang == 'typescript'
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install pnpm
if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }}
if: matrix.lang == 'typescript'
uses: ./.github/actions/setup-pnpm
with:
run_install: true

- name: Build TypeScript SDK
if: ${{ github.event_name != 'workflow_dispatch' || contains(inputs.languages || 'rust,csharp,typescript', 'typescript') }}
if: matrix.lang == 'typescript'
run: pnpm build
working-directory: crates/bindings-typescript

Expand All @@ -119,16 +141,19 @@ jobs:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
MSBUILDDISABLENODEREUSE: "1"
DOTNET_CLI_USE_MSBUILD_SERVER: "0"
LLM_BENCH_CSHARP_CONCURRENCY: "1"
INPUT_LANGUAGES: ${{ inputs.languages || 'rust,csharp,typescript' }}
LLM_BENCH_CONCURRENCY: "8"
LLM_BENCH_RUST_CONCURRENCY: "4"
LLM_BENCH_CSHARP_CONCURRENCY: "2"
LLM_BENCH_ROUTE_CONCURRENCY: ${{ matrix.lang == 'typescript' && '4' || '2' }}
INPUT_LANGUAGE: ${{ matrix.lang }}
INPUT_MODEL_SET: ${{ inputs.model_set || 'website_active' }}
INPUT_MODELS: ${{ inputs.models || '' }}
INPUT_MODES: ${{ inputs.modes || 'guidelines,no_context' }}
INPUT_CATEGORIES: ${{ inputs.categories || '' }}
INPUT_TASKS: ${{ inputs.tasks || '' }}
INPUT_DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
LANGS="$INPUT_LANGUAGES"
LANG="$INPUT_LANGUAGE"
MODEL_SET="$INPUT_MODEL_SET"
MODELS="$INPUT_MODELS"
MODES="$INPUT_MODES"
Expand Down Expand Up @@ -162,45 +187,21 @@ jobs:
;;
esac

SUCCEEDED=0
FAILED=0
for LANG in $(echo "$LANGS" | tr ',' ' '); do
EXTRA_ARGS=()
if [ -n "$CATEGORIES" ]; then
EXTRA_ARGS+=(--categories "$CATEGORIES")
fi
if [ -n "$TASKS" ]; then
EXTRA_ARGS+=(--tasks "$TASKS")
fi
if [ "$DRY_RUN" = "true" ]; then
EXTRA_ARGS+=(--dry-run)
fi

if [ "$MODEL_SET" = "website_active" ]; then
if llm_benchmark run --lang "$LANG" --modes "$MODES" --model-source remote "${EXTRA_ARGS[@]}"; then
SUCCEEDED=$((SUCCEEDED + 1))
else
echo "::warning::Benchmark run failed for lang=$LANG"
FAILED=$((FAILED + 1))
fi
elif [ "$MODEL_SET" = "local_defaults" ]; then
if llm_benchmark run --lang "$LANG" --modes "$MODES" "${EXTRA_ARGS[@]}"; then
SUCCEEDED=$((SUCCEEDED + 1))
else
echo "::warning::Benchmark run failed for lang=$LANG"
FAILED=$((FAILED + 1))
fi
else
if llm_benchmark run --lang "$LANG" --modes "$MODES" --models "${MODEL_ARGS[@]}" "${EXTRA_ARGS[@]}"; then
SUCCEEDED=$((SUCCEEDED + 1))
else
echo "::warning::Benchmark run failed for lang=$LANG models=$MODELS"
FAILED=$((FAILED + 1))
fi
fi
done
echo "Benchmark runs: $SUCCEEDED succeeded, $FAILED failed"
if [ "$FAILED" -gt 0 ]; then
echo "::error::$FAILED benchmark run(s) failed"
exit 1
EXTRA_ARGS=()
if [ -n "$CATEGORIES" ]; then
EXTRA_ARGS+=(--categories "$CATEGORIES")
fi
if [ -n "$TASKS" ]; then
EXTRA_ARGS+=(--tasks "$TASKS")
fi
if [ "$DRY_RUN" = "true" ]; then
EXTRA_ARGS+=(--dry-run)
fi

if [ "$MODEL_SET" = "website_active" ]; then
llm_benchmark run --lang "$LANG" --modes "$MODES" --model-source remote "${EXTRA_ARGS[@]}"
elif [ "$MODEL_SET" = "local_defaults" ]; then
llm_benchmark run --lang "$LANG" --modes "$MODES" "${EXTRA_ARGS[@]}"
else
llm_benchmark run --lang "$LANG" --modes "$MODES" --models "${MODEL_ARGS[@]}" "${EXTRA_ARGS[@]}"
fi
109 changes: 105 additions & 4 deletions skills/csharp-server/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ metadata:

Reducers are static methods in a `static partial class`; tables are `public partial struct`s. This reference keeps everything in one `public static partial class Module`, which needs only `using SpacetimeDB;`:

Methods exported through SpacetimeDB attributes, including reducers, procedures, views, HTTP handlers, and routers, must be `public static`; generated bindings invoke them from another class.

```csharp
using SpacetimeDB;

Expand Down Expand Up @@ -51,8 +53,10 @@ public partial struct Entity
[PrimaryKey]
[AutoInc]
public ulong Id;
[SpacetimeDB.Index.BTree]
public Identity Owner;
public string Name;
[SpacetimeDB.Index.BTree]
public bool Active;
}
```
Expand Down Expand Up @@ -90,8 +94,11 @@ The complete set of column attributes:
[AutoInc] // auto-increment (use 0 as placeholder on insert)
[Unique] // unique constraint; indexes the column, enables .Find()
[SpacetimeDB.Index.BTree] // btree index (enables .Filter() on this column)
[Default(true)] // migration-safe default for a newly appended field
```

Defaults support compatible addition of a newly appended field. Do not apply `[Default(...)]` to primary-key, unique, or auto-increment fields.

## Indexes

Write the index attribute fully qualified: `[SpacetimeDB.Index.BTree]`. Prefer inline for single-column; multi-column uses struct-level:
Expand Down Expand Up @@ -155,24 +162,53 @@ public static void OnConnect(ReducerContext ctx) { ... }
public static void OnDisconnect(ReducerContext ctx) { ... }
```

`ctx.ConnectionId` is `ConnectionId?`, including in connection lifecycle reducers. Check or unwrap it before storing it in a non-nullable column or passing it to an index accessor.

## Views

```csharp
// Anonymous view (same result for all clients):
[SpacetimeDB.View(Accessor = "ActiveUsers", Public = true)]
public static List<Entity> ActiveUsers(AnonymousViewContext ctx)
{
return ctx.Db.Entity.Iter().Where(e => e.Active).ToList();
return ctx.Db.Entity.Active.Filter(true).ToList();
}

// Per-user view:
[SpacetimeDB.View(Accessor = "MyProfile", Public = true)]
public static Entity? MyProfile(ViewContext ctx)
[SpacetimeDB.View(Accessor = "MyEntities", Public = true)]
public static List<Entity> MyEntities(ViewContext ctx)
{
return ctx.Db.Entity.Identity.Find(ctx.Sender) as Entity?;
return ctx.Db.Entity.Owner.Filter(ctx.Sender).ToList();
}
```

View contexts expose read-only table handles. These support `Count` plus `Find`/`Filter` on declared indexes, but not full-table `Iter()`. Start procedural view traversal from an indexed lookup or filter.

Query-builder views use `ViewContext`, `ctx.From`, and return `IQuery<T>` directly. Use `Where` for predicates and `RightSemijoin` when the result should contain right-side rows that have a matching left-side row:

```csharp
ctx.From.Article().Where(article => article.Published.Eq(true));
ctx.From.Subscription().RightSemijoin(
ctx.From.Account(),
(subscription, account) => subscription.AccountId.Eq(account.Id)
);
```

Declare a procedural view primary key in its attribute: `[SpacetimeDB.View(Accessor = "CatalogEntry", Public = true, PrimaryKey = nameof(CatalogRow.Sku))]`.

Procedural-view primary keys are explicit schema metadata. Add one only when the view itself is required to expose a primary key; a source table's primary key is not inherited by the view.

Inclusive btree ranges use tuples, for example `ctx.Db.Shipment.DeliverBy.Filter((new Timestamp(1_000), new Timestamp(2_000)))`.

## Client Visibility Filters

```csharp
[ClientVisibilityFilter]
public static readonly Filter PrivateNoteFilter = new Filter.Sql(
"SELECT * FROM OwnedRow WHERE Owner = :sender"
);
```

## Reducer Context API

`ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules.
Expand Down Expand Up @@ -229,6 +265,71 @@ var at = new ScheduleAt.Interval(TimeSpan.FromSeconds(5));
ctx.Db.TickTimer.Insert(new TickTimer { ScheduledId = 0, ScheduledAt = at });
```

Scheduled reducer callbacks use the ordinary `[SpacetimeDB.Reducer]` attribute. There is no `ReducerKind.Scheduled`; the table's `Scheduled` option associates the callback.

To construct a `ConnectionId` from a 128-bit numeric representation, encode it as exactly 16 little-endian bytes and call `ConnectionId.From(bytes)`. The result is nullable and must be checked before use.

## Procedures and HTTP

Procedures are unstable APIs, so modules using them should include `#pragma warning disable STDB_UNSTABLE`. They receive `ProcedureContext` and may return `[SpacetimeDB.Type]` values:

```csharp
[SpacetimeDB.Type]
public partial struct ResultValue { public string Value; }

[SpacetimeDB.Procedure]
public static ResultValue Inspect(ProcedureContext ctx, string input) =>
new() { Value = input };
```

Outbound HTTP is available through `ctx.Http`; handle its success/error result before using the response. Open short database transactions with `ctx.WithTx`. Perform network I/O before opening the transaction, and keep only database work inside its callback.

```csharp
var result = ctx.Http.Get(uri);
var text = result.Match(
response => response.Body.ToStringUtf8Lossy(),
error => throw new Exception(error.Message)
);

ctx.WithTx(tx =>
{
tx.Db.ScoreRecord.Insert(new ScoreRecord { Id = 0, Owner = ctx.Sender, Value = 1 });
return 0;
});
```

`WithTx` is generic: its callback must return a value (use `return 0;` when no result is needed). The callback's generated `tx.Db` exposes module table accessors; `ProcedureContext` and `HandlerContext` do not expose tables directly.

For non-GET requests, construct and send an `HttpRequest` directly:

```csharp
var result = ctx.Http.Send(new HttpRequest
{
Uri = uri,
Method = SpacetimeDB.HttpMethod.Post,
Headers = new() { new HttpHeader("content-type", "text/plain") },
Body = HttpBody.FromString(payload),
});
```

`HttpResponse.StatusCode` is `ushort`. HTTP headers are `HttpHeader` values, not tuples, and each header's `Value` is `byte[]`; decode text values with `System.Text.Encoding.UTF8.GetString(header.Value)`. Treat bodies as bytes: use `HttpBody.FromString(...)` to create text, `new HttpBody(bytes)` to supply raw bytes, `ToBytes()` to read raw bytes, and `ToStringUtf8Lossy()` to read text. There is no `HttpBody.FromBytes`, and `ToString()` does not return body contents. Qualify `SpacetimeDB.HttpMethod` when .NET's implicit `System.Net.Http` imports could make the name ambiguous.

Scheduled procedures use the ordinary scheduled-table shape. Its `Scheduled` name refers to a `[SpacetimeDB.Procedure]` method taking `ProcedureContext` plus the scheduled row, and database access inside that procedure goes through `ctx.WithTx`.

Inbound HTTP uses handler attributes and one router. Handler database access also goes through `ctx.WithTx`:

```csharp
[SpacetimeDB.HttpHandler]
public static HttpResponse Health(HandlerContext ctx, HttpRequest request) => new(
200, HttpVersion.Http11, new(), HttpBody.FromString("ok")
);

[SpacetimeDB.HttpRouter]
public static Router Routes() => SpacetimeDB.Router.New().Get("/health", Handlers.Health);
```

`Handlers` is generated from methods marked `[SpacetimeDB.HttpHandler]`; do not declare it yourself. Reference an attributed method in a router as `Handlers.MethodName`.

## Custom Types

```csharp
Expand Down
Loading
Loading