<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Charles Chen on Medium]]></title>
        <description><![CDATA[Stories by Charles Chen on Medium]]></description>
        <link>https://medium.com/@chrlschn?source=rss-61db58b8ec88------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*IA8XtX5yhEEYbj6mZZ7bTQ.jpeg</url>
            <title>Stories by Charles Chen on Medium</title>
            <link>https://medium.com/@chrlschn?source=rss-61db58b8ec88------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 09:53:50 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@chrlschn/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[The Unexpected AI Stack: C# + .NET (Part 4)]]></title>
            <link>https://itnext.io/the-unexpected-ai-stack-c-net-part-4-ca6219e73b7f?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/ca6219e73b7f</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[artificial-intelligence]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Sun, 13 Sep 2026 17:41:34 GMT</pubDate>
            <atom:updated>2026-09-13T22:07:26.613Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*fxr34uI6_VGqOClj.png" /><figcaption>Scaffolding application storage and streamlining automated agent testing.</figcaption></figure><p>Setting up the test foundations using Testcontainers and transactions for AI-enabled applications with C# and .NET</p><h3>Summary</h3><ul><li>An effective testing strategy is part of the scaffolding for a foundational codebase that allows agents to operate with guardrails and checks.</li><li>Testcontainers and automatic test-scoped transactions allow agents to write and execute database level integration tests in a stateless manner with isolation and parallelism.</li><li>To help short circuit per-run agent discovery of how to operate the test harness, we interactively produce a skill with the agent on how to use write and operate the test infrastructure.</li><li><a href="https://medium.com/itnext/the-unexpected-ai-stack-c-net-part-1-5a45f46a1ff1"><strong>Part 1</strong></a> will introduce two under-the-radar components of the .NET stack that make it surprisingly amenable to building software with agents: Aspire and CSharpRepl.</li><li><a href="https://medium.com/itnext/the-unexpected-ai-stack-c-net-part-2-2dddd1e36fab"><strong>Part 2</strong> </a>will dive into a hands-on implementation from the ground up to scaffold an open-source template for teams to build on top of.</li><li><a href="https://medium.com/itnext/the-unexpected-ai-stack-c-net-part-3-fba953e01143"><strong>Part 3</strong></a> will implement the next layer of the application including a simple streaming interface to the Copilot SDK agent.</li><li><strong>Part 4</strong> (👈 you are here) will extend the application with Testcontainers to demonstrate how to simplify test execution for agents with stateless containers.</li><li><em>Part 5</em> will configure the application with logging, telemetry, and observability before diving into building the actual application using your coding agent.</li></ul><p><strong><em>The full repo</em></strong>: <a href="https://github.com/zeeq-ai/zeeq-tmpl/tree/feat-part4-Part-4-code-changes">https://github.com/zeeq-ai/zeeq-tmpl/tree/feat-part4-Part-4-code-changes</a> (note the branch; <a href="https://github.com/zeeq-ai/zeeq-tmpl/pull/2">see the PR for the diff</a>)</p><h3>What we’re building</h3><p>The previous part of this series walked through the construction of an AI enabled application that connects a web-based chat frontend to an instance of the Copilot Agent via the SDK that allows us to work with the codebase from any web API. This can be a foundation for building your own collaborative agent meta-harness on top of a model agnostic agent harness.</p><p>The exercises have already walked through examples of how to leverage Aspire and CSharpRepl, but the focus for this series is still on understanding the mechanical scaffolding that a platform team has to undertake to build a solid foundation for an agentic engineering team to accelerate. The posts intentionally focus on how to build each piece and how the pieces are connected together as these are the core components of a platform.</p><p>There are two major pieces left to scaffold before we let loose with agents. In this part, we’ll scaffold the testing infrastructure which will let the agents verify their code using a stateless containerized database and transaction rollback to ensure that the tests are isolated. In the next part, we’ll add logging, telemetry, and observability to the application so that we can see what the agents are doing in real time.</p><h3>Scaffolding for storage</h3><p>For local use, SQLite or even the local file system are suitable for storing the user specifications, but to demonstrate how to use Postgres and Testcontainers, we’ll add a Postgres database to the application.</p><h3>Adding database access to the application</h3><p>First is to add the packages necessary to connect to Postgres and set up the connection string</p><pre>cd src/server<br>dotnet add package EFCore.NamingConventions<br>dotnet add package Microsoft.EntityFrameworkCore.Design<br>dotnet add package Microsoft.EntityFrameworkCore.Relational<br>dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL</pre><p>The Entity Framework Core DbContext is first:</p><pre>// src/server/Data/ZeeqContext.cs<br>using Microsoft.EntityFrameworkCore;<br><br>namespace Zeeq.Tmpl;<br><br>public class ZeeqContext(DbContextOptions&lt;ZeeqContext&gt; options) : DbContext(options)<br>{<br>    protected override void OnModelCreating(ModelBuilder modelBuilder)<br>    {<br>        // 👇 Applying the entity configurations only from this assembly; modify as needed<br>        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ZeeqContext).Assembly);<br>    }<br>}</pre><p>And add this into the DI container at setup:</p><pre>// src/server/Program.cs<br><br>// 👇 This will be injected by Aspire when we connect the resources<br>var connectionString = Environment.GetEnvironmentVariable(&quot;ConnectionStrings__zeeq&quot;);<br><br>// Set up the database context<br>builder.Services.AddDbContext&lt;ZeeqContext&gt;(options =&gt;<br>    options<br>        .UseNpgsql(connectionString)<br>        .EnableDetailedErrors(true)<br>        .EnableSensitiveDataLogging(true)<br>        .UseSnakeCaseNamingConvention()<br>);</pre><h3>Setting up the model</h3><p>Now we’ll set up the model for storing the specifications:</p><pre>// src/server/Data/Models.cs<br>using System.ComponentModel.DataAnnotations;<br>using Microsoft.EntityFrameworkCore;<br>using Microsoft.EntityFrameworkCore.Metadata.Builders;<br><br>namespace Zeeq.Tmpl;<br><br>/// &lt;summary&gt;<br>/// A pure domain model class for storing the specification<br>/// &lt;/summary&gt;<br>public class Specification<br>{<br>    public Guid Id { get; set; }<br><br>    [MaxLength(255)] // 👈 Note the validators here<br>    public string Name { get; set; } = string.Empty;<br><br>    [MaxLength(10000)]<br>    public string Content { get; set; } = string.Empty;<br>    public long TokenCount { get; set; }<br>    public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;<br>    public DateTime? UpdatedAtUtc { get; set; }<br>}<br><br>/// &lt;summary&gt;<br>/// The database storage configuration for the model.  This is separated from the<br>/// pure domain model and represents the storage mapping for the ORM.  When scaling<br>/// the codebase, this can be placed with a dedicated DB project instead of in the<br>/// same codebase<br>/// &lt;/summary&gt;<br>public class SpecificationConfiguration : IEntityTypeConfiguration&lt;Specification&gt;<br>{<br>    public void Configure(EntityTypeBuilder&lt;Specification&gt; builder)<br>    {<br>        builder.HasKey(s =&gt; s.Id);<br>        builder.Property(s =&gt; s.Name).IsRequired();<br>        builder.Property(s =&gt; s.Content).IsRequired();<br>        builder.Property(s =&gt; s.TokenCount).IsRequired();<br>        builder.Property(s =&gt; s.CreatedAtUtc).IsRequired();<br>        builder.Property(s =&gt; s.UpdatedAtUtc);<br>    }<br>}</pre><h3>Setting up Postgres in Aspire</h3><p>To add <a href="https://aspire.dev/integrations/databases/postgres/postgres-host/">Postgres into the stack</a>, we’ll add a new resource using the Aspire CLI:</p><pre># From root to add the Postgresql package to /host<br>aspire add postgresql</pre><p>After adding this, we can add the resource to the stack:</p><pre>// host/AppHost.cs<br><br>// Add the resource first<br>var username = builder.AddParameter(&quot;username&quot;, &quot;zeeq&quot;, secret: true);<br>var password = builder.AddParameter(&quot;password&quot;, &quot;P@ssw0rd&quot;, secret: true);<br>var postgres = builder<br>    .AddPostgres(&quot;postgres&quot;, userName: username, password: password)<br>    .AddDatabase(&quot;zeeq&quot;)<br><br>// Wire it to the backend resource so it gets the connection string<br>var backend = builder<br>    .AddProject&lt;Projects.server&gt;(&quot;app-backend&quot;)<br>    // 👇 Wire up CSharpRepl environment variables to the runtime.<br>    .WithEnvironment(&quot;DOTNET_STARTUP_HOOKS&quot;, csrHook)<br>    .WithEnvironment(&quot;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&quot;, &quot;CSharpRepl.InjectedHook&quot;)<br>    // 👇 Connect the postgres instance<br>    .WithReference(postgres);</pre><p>After we wire up Postgres to the backend, Aspire automatically injects the connection string into the backend as an environment variable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1021/1*p0S14jPVEjbar9h3pkZ0zQ.png" /><figcaption><em>Here, we can see Aspire wires the resources together by injecting the connection string into the backend as an environment variable.</em></figcaption></figure><h3>Create the database migration</h3><p>Before moving on, we’ll create the database migration that will be needed by the test fixture in the next section.</p><pre>cd src/server<br>dotnet ef migrations add Zeeq_Planner_Init</pre><p>Our database scaffolding is ready; next is to scaffold the test infrastructure that we need to allow agents to run integration tests in an isolated manner using Testcontainers.</p><h3>Adding Testcontainers for integration testing</h3><p>For testing the application, we’ll add a new test project with Testcontainers and TUnit.</p><h3>Adding the Testcontainer and TUnit packages</h3><p>We’ll add the packages and add a smoke test to ensure that everything is working and wired up:</p><pre>cd src<br>mkdir tests<br>cd tests<br>dotnet new console<br>dotnet add package TUnit<br>dotnet add package Testcontainers.PostgreSql<br>dotnet add reference ../server<br>mv Program.cs SmokeTest.cs<br><br>cd ../../<br>dotnet sln add src/tests</pre><p>In global.json, we’ll need to instruct the runtime that we’re using Microsoft Testing Platform (MTP) mode:</p><pre>// global.json<br>{<br>  &quot;sdk&quot;: {<br>    &quot;version&quot;: &quot;10.0.302&quot;,<br>    &quot;rollForward&quot;: &quot;latestFeature&quot;<br>  },<br>  &quot;test&quot;: {<br>    &quot;runner&quot;: &quot;Microsoft.Testing.Platform&quot;<br>  }<br>}</pre><p>Add a smoke test:</p><pre>// src/tests/SmokeTest.cs<br>public class SmokeTest<br>{<br>    [Test]<br>    public async Task TestSmoke()<br>    {<br>        await Assert.That(true).IsTrue();<br>    }<br>}</pre><p>Now we can run the test:</p><pre>dotnet test<br><br>Test run summary: Passed!<br>  total: 1<br>  failed: 0<br>  succeeded: 1<br>  skipped: 0<br>  duration: 717ms</pre><h3>Wiring up a database fixture and transaction rollback</h3><p>Before we let the coding agent loose to build the backend, we’re going to manually scaffold the Testcontainer test fixture and also set up the transaction rollback so that integration tests remain isolated.</p><p>First up is setting up the Testcontainer:</p><pre>// src/tests/PgDatabaseFixture.cs<br>using DotNet.Testcontainers.Builders;<br>using Microsoft.EntityFrameworkCore;<br>using Testcontainers.PostgreSql;<br>using TUnit.Core.Interfaces;<br>using Zeeq.Tmpl;<br><br>/// &lt;summary&gt;<br>/// TUnit fixture for setting up the Postgres Testcontainer database<br>/// &lt;/summary&gt;<br>public class PgDatabaseFixture : IAsyncInitializer, IAsyncDisposable<br>{<br>    /// &lt;summary&gt;<br>    /// The container instance for this fixture.<br>    /// &lt;/summary&gt;<br>    private PostgreSqlContainer? _container;<br>    private DbContextOptions&lt;ZeeqContext&gt; _options = null!;<br><br>    private DbContextOptions&lt;ZeeqContext&gt; EnsureOptions()<br>    {<br>        if (_container is null)<br>        {<br>            throw new InvalidOperationException(&quot;Postgres container is not initialized.&quot;);<br>        }<br><br>        _options ??= new DbContextOptionsBuilder&lt;ZeeqContext&gt;()<br>            .UseNpgsql(_container.GetConnectionString())<br>            .EnableDetailedErrors(true)<br>            .EnableSensitiveDataLogging(true)<br>            .UseSnakeCaseNamingConvention()<br>            .Options;<br><br>        return _options;<br>    }<br><br>    public ZeeqContext CreateContext() =&gt; new(EnsureOptions());<br><br>    public async Task InitializeAsync()<br>    {<br>        // 👇 Initialize the Postgres container<br>        _container = new PostgreSqlBuilder(&quot;postgres:18&quot;)<br>            .WithPortBinding(5432, true)<br>            .WithPassword(&quot;password&quot;)<br>            .WithUsername(&quot;username&quot;)<br>            .WithDatabase(&quot;zeeq&quot;)<br>            .WithWaitStrategy(<br>                Wait.ForUnixContainer()<br>                    .UntilMessageIsLogged(&quot;database system is ready to accept connections&quot;)<br>                    .UntilInternalTcpPortIsAvailable(5432)<br>            )<br>            .WithAutoRemove(true)<br>            .Build();<br><br>        await _container.StartAsync();<br><br>        // 👇 Run the migrations to set up the database schema<br>        using var context = new ZeeqContext(EnsureOptions());<br><br>        await context.Database.EnsureCreatedAsync();<br>    }<br><br>    public async ValueTask DisposeAsync()<br>    {<br>        if (_container is not null)<br>        {<br>            await _container.DisposeAsync();<br>        }<br><br>        GC.SuppressFinalize(this);<br><br>        await ValueTask.CompletedTask;<br>    }<br>}</pre><p>This will create one container per test project (only one in this case) and can help scale test execution throughput.</p><p>To wrap the tests with a transaction, we’ll use a base class that has two methods that wrap the execution of each test method:</p><pre>// src/tests/PgTransactionalTestBase.cs<br>using Microsoft.EntityFrameworkCore.Storage;<br>using Zeeq.Tmpl;<br><br>/// &lt;summary&gt;<br>/// A base class that provides access to creating a context and transaction scoping<br>/// around the operation.<br>/// &lt;/summary&gt;<br>public abstract class PgTransactionalTestBase(PgDatabaseFixture pg)<br>{<br>    private IDbContextTransaction? _transaction;<br><br>    // 👇 Each test instantiates a new class instance so we can use class-level state<br>    protected ZeeqContext _context = pg.CreateContext();<br><br>    [Before(Test)] // 👈 Start a transaction before the test<br>    public async Task Before()<br>    {<br>        _transaction = await _context.Database.BeginTransactionAsync();<br>    }<br><br>    [After(Test)] // 👈 Roll it back so nothing is committed to state<br>    public async Task Cleanup()<br>    {<br>        if (_transaction != null)<br>        {<br>            await _transaction.RollbackAsync();<br>            await _transaction.DisposeAsync();<br>        }<br><br>        await _context.DisposeAsync();<br>    }<br>}</pre><p>Now our test classes inherit from this base class and every test is wrapped in a transaction that is rolled back after the test completes.</p><p>A basic CRUD test to make sure everything is working:</p><pre>// src/tests/SpecificationTests.cs<br>using Microsoft.EntityFrameworkCore;<br>using Zeeq.Tmpl;<br><br>// 👇 Wire the fixture and forward to the base class<br>[ClassDataSource&lt;PgDatabaseFixture&gt;(Shared = SharedType.PerTestSession)]<br>public class SpecificationTests(PgDatabaseFixture pg) : PgTransactionalTestBase(pg)<br>{<br>    [Test]<br>    public async Task Specification_Basic_WriteRead()<br>    {<br>        _context.Set&lt;Specification&gt;().Add(new() { Name = &quot;Test Spec&quot; });<br><br>        await _context.SaveChangesAsync();<br><br>        _context.ChangeTracker.Clear(); // 👈 Clear EF change tracking and reload from DB<br><br>        var spec = await _context.Set&lt;Specification&gt;().FirstOrDefaultAsync();<br><br>        await Assert.That(spec).IsNotNull();<br>        await Assert.That(spec!.Name).IsEqualTo(&quot;Test Spec&quot;);<br>        await Assert.That(spec.Id).IsNotEqualTo(Guid.Empty); // 👈 Set on write<br>    }<br>}</pre><p>Now if we run dotnet test:</p><pre>Test run summary: Passed!<br>  total: 2<br>  failed: 0<br>  succeeded: 2<br>  skipped: 0<br>  duration: 3s 198ms</pre><p>Looks good!</p><h3>Training the agent to use the test harness</h3><p>By default, the agent will try to use XUnit commands because TUnit is a relatively newer framework with less representation in the model training. Additionally, the Microsoft Testing Platform (MTP) is also a relatively new test runner that is not as widely represented in the model training.</p><p>To help the agent understand how to run the tests, we’ll go through the same exercise as before with CSharpRepl and guide the agent through some experiments with some starting points for it to read so it can materialize a skill for testing.</p><pre>&lt;!-- Give it some basics about TUnit and specifically mention MTP --&gt;<br>We are working on writing a skill in .agents/skills/unit-integration-testing/SKILL.md for .NET and C# code<br>Our goal is to keep this skill short, terse, to the point with key examples broken down into sections.<br>The test project is in src/tests using TUnit (NOT XUnit) and Microsoft Testing Platform<br>See: https://tunit.dev/docs/execution/test-filters<br>See: https://tunit.dev/docs/assertions/getting-started<br>See: https://tunit.dev/docs/migration/xunit<br>See: https://tunit.dev/docs/reference/command-line-flags<br>Write a quick introductory section and then a table of the key basic commands.<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Document key assertion patterns --&gt;<br>Document the key assertion patterns supported by TUnit<br>See: https://tunit.dev/docs/assertions/exceptions<br>See: https://tunit.dev/docs/assertions/combining-assertions<br>See: https://tunit.dev/docs/assertions/equality-and-comparison<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Specifically call out the PostgresDbFixture and transactional behavior --&gt;<br>See: PgDatabaseFixture.cs, PgTransactionalTestBase.cs to understand how the test database is set up<br>Document a bullet list of notes for the test scaffolding that is relevant to understanding how to write integration tests<br>Capture information about test isolation behavior for integration tests that touch the database<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Now have it run a filtered test (if it did not already produce this) --&gt;<br>Use treenode filters to select a single test file to run<br>Use treenode filters to select a single test method to run<br>Document your findings and fill any gaps in the document<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Add instructions for how to write good tests --&gt;<br>Document best practices for writing good tests:<br>- High signal, low noise<br>- Focus on invariants<br>- Use class-level setup for shared resources; each test runs as class instance<br>- Use three part naming convention for test methods: EntityOrScopeName_StateUnderTest_ExpectedBehavior</pre><p>We’ll add a short note in AGENTS.md for both the CSharpRepl skill as well as the test skill.</p><pre>&lt;!-- AGENTS.md --&gt;<br>## Skills<br><br>- Use the skill `unit-integration-testing` when writing and running .NET C# tests<br>- Use the skill `csharprepl` to directly manipulate the running C# application, wrap methods, replace methods, and inspect the runtime state of the application</pre><p>This way the agent knows exactly when to use these two skills.</p><h3>Closing thoughts</h3><p>This stage of the exercise continues to focus on the key choices that human operators need to make when scaffolding the foundational codebase that agents will code on top of. Because these decisions will ultimately affect correctness, iteration speed, and operational runtime correctness, each one has been made intentionally to understand the guardrails that are in place to help agents work effectively.</p><p>Testcontainers and transaction rollback are a key part of this by providing parallelism for large projects as well as test isolation. Using Aspire and EF Core, it’s easy to wire together a fully type-safe data layer with a domain model isolated from the underlying storage mapping.</p><p>In the last and final part, we’ll wire up logging, telemetry, and observability to the application and build the final specification writing surface that will allow the agent to give continuous feedback on the specification as it runs research on the underlying code as the specification is being written.\</p><blockquote><em>If you are curious to see a real-world setup, check out the </em><a href="https://zeeq.ai/"><em>Zeeq.ai</em></a><em> repo: </em><a href="https://github.com/zeeq-ai/zeeq-app"><em>https://github.com/zeeq-ai/zeeq-app</em></a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ca6219e73b7f" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/the-unexpected-ai-stack-c-net-part-4-ca6219e73b7f">The Unexpected AI Stack: C# + .NET (Part 4)</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Unexpected AI Stack: C# + .NET (Part 3)]]></title>
            <link>https://itnext.io/the-unexpected-ai-stack-c-net-part-3-fba953e01143?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/fba953e01143</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[design]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Thu, 27 Aug 2026 12:08:25 GMT</pubDate>
            <atom:updated>2026-08-27T13:10:14.372Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*r9H-LvhM9fFtXBY8.png" /><figcaption>Part 3: Wiring up the front-end and the CSharpRepl skill</figcaption></figure><h3>Summary</h3><ul><li>The application makes several key architectural decisions to make it easier for agents to work with and iterate like using OpenAPI (stable, easy for agents to understand and work with, contract-based) with minimal customization.</li><li>This is wired using standard .NET tooling and Kubb.dev on the frontend to generate the TypeScript API clients</li><li>Using the Copilot SDK allows us to build a powerful agentic application with minimal code (just a few lines of setup). Using .NET’s built in System.Threading.Channels allows the application to queue inputs and stream outputs to the frontend — fully typed end-to-end.</li><li>To build the skill for CSharpRepl, use your coding agent and “teach” it how to use it correctly rather than re-discovering this each time. YMMV based on your model, your harness, and the complexity of your application DI setup so it is better to use this approach than to provide a canned example.</li></ul><h3>What we’re building</h3><p>This exercise will build a simple chat application connected to an instance of a Copilot agent via the <a href="https://github.com/github/copilot-sdk">Copilot SDK</a>.</p><p>The Copilot SDK is a good choice for building agentic applications because:</p><ul><li>The SDK supports multiple language bindings including TypeScript, Python, Go, C#, Rust, and Java</li><li>The harness is model agnostic and allows using different models for different tasks and BYOK or use the GitHub Copilot subscription (we’ll be using BYOK for this exercise)</li><li>The SDK is well documented and comes with advanced features for working with and navigating source code.</li></ul><p>For the front-end, we’ll build a chat interface using a Vite + Nuxt UI web app using a streaming API. We’ll use <a href="https://kubb.dev/">Kubb.dev</a> for client generation from OpenAPI specs.</p><h3>Wiring OpenAPI</h3><p>Before continuing on and letting the agents loose, we still have a bit of scaffolding to do <em>(this can be moved into a skill, but the purpose of this exercise is to understand the underlying wiring before building a skill template)</em>.</p><h3>Update the .csproj to produce the OpenAPI schema</h3><p>We’ll need to add a package to the src/server/server.csproj to emit the OpenAPI spec:</p><pre>cd src/server<br>dotnet add package Microsoft.Extensions.ApiDescription.Server</pre><p>A few tweaks are also needed to the .csproj</p><pre>&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;<br>  &lt;PropertyGroup&gt;<br>    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;<br>    &lt;Nullable&gt;enable&lt;/Nullable&gt;<br>    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;<br>    &lt;RootNamespace&gt;Zeeq.Tmpl&lt;/RootNamespace&gt;<br>    &lt;UserSecretsId&gt;db43be79-6f94-4d75-9ee0-45d496cfae4d&lt;/UserSecretsId&gt;<br>    &lt;!-- 👇 OpenAPI config --&gt;<br>    &lt;OpenApiDocumentsDirectory&gt;..\app\src\api&lt;/OpenApiDocumentsDirectory&gt;<br>    &lt;OpenApiGenerateDocumentsOptions&gt;--file-name zeeq-tmpl-api&lt;/OpenApiGenerateDocumentsOptions&gt;<br>  &lt;/PropertyGroup&gt;<br>  &lt;!-- 👇 Don&#39;t generate in the Release build (should already be present) --&gt;<br>  &lt;PropertyGroup Condition=&quot;&#39;$(Configuration)&#39; == &#39;Release&#39;&quot;&gt;<br>    &lt;!-- Suppress OpenAPI document generation in Release; the target checks this property --&gt;<br>    &lt;OpenApiGenerateDocuments&gt;false&lt;/OpenApiGenerateDocuments&gt;<br>  &lt;/PropertyGroup&gt;<br>  &lt;ItemGroup&gt;<br>    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.OpenApi&quot; Version=&quot;10.0.11&quot; /&gt;<br>    &lt;!-- 👇 Added via the `dotnet add package ...` --&gt;<br>    &lt;PackageReference Include=&quot;Microsoft.Extensions.ApiDescription.Server&quot; Version=&quot;10.0.11&quot;&gt;<br>      &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers; buildtransitive&lt;/IncludeAssets&gt;<br>      &lt;PrivateAssets&gt;all&lt;/PrivateAssets&gt;<br>    &lt;/PackageReference&gt;<br>    &lt;PackageReference Include=&quot;Scrutor&quot; Version=&quot;7.0.0&quot; /&gt;<br>  &lt;/ItemGroup&gt;<br>&lt;/Project&gt;</pre><p>Now running dotnet build src/server will produce the OpenAPI schema in src/app/src/api/zeeq-tmpl-api.json.</p><h3>Wire up Kubb.dev to generate the API client from the schema</h3><p>Kubb is a TypeScript code generator that uses a plugin architecture to produce client bindings, fakes, validators, and more using the OpenAPI schema.</p><p>We’ll use this to generate the API client for the frontend.</p><pre>cd src/app<br><br># Note: this is using 5.0.0-beta.103<br>pnpm add -D kubb @kubb/plugin-ts @kubb/plugin-fetch</pre><p>Now to configure it:</p><pre>// src/app/kubb.config.ts<br>import { defineConfig } from &quot;kubb&quot;;<br>import { pluginTs } from &quot;@kubb/plugin-ts&quot;;<br>import { pluginFetch } from &quot;@kubb/plugin-fetch&quot;;<br><br>export default defineConfig({<br>  input: &quot;./src/api/zeeq-tmpl-api.json&quot;,<br>  output: {<br>    path: &quot;./src/api/generated&quot;,<br>    clean: true,<br>  },<br>  plugins: [<br>    pluginTs({<br>      output: {<br>        path: &quot;./types&quot;,<br>        mode: &quot;directory&quot;<br>      },<br>      enum: {<br>        type: &quot;asConst&quot;<br>      },<br>      optionalType: &quot;questionTokenAndUndefined&quot;,<br>    }),<br>    pluginFetch({<br>      baseURL: &quot;http://localhost:5138&quot;, // 👈 From the launch profile for the backend<br>      output: {<br>        path: &quot;./clients&quot;,<br>        mode: &quot;directory&quot;,<br>      },<br>    }),<br>  ],<br>});</pre><p>The package.json will need two scripts:</p><pre>// src/app/package.json<br>{<br>  &quot;scripts&quot;: {<br>    &quot;generate&quot;: &quot;kubb generate&quot;,<br>    &quot;generate:watch&quot;: &quot;kubb generate --watch&quot;<br>  }<br>}</pre><p>Now if we run:</p><pre># Manually generate to smoke test the wiring<br>cd src/app<br>pnpm generate</pre><p>This will produce an updated API client in src/app/src/api/generated (recommend adding to your .gitignore).</p><h3>Automating with Aspire</h3><p>To automate this generation when the schema updates on build, add another resource to Aspire that watches the OpenAPI schema file and runs the Kubb generation command.</p><pre>// host/AppHost.cs<br>var kubb = builder<br>    .AddExecutable(<br>        name: &quot;kubb-generate-watch&quot;,<br>        command: &quot;pnpm&quot;,<br>        workingDirectory: &quot;../src/app&quot;,<br>        args: [&quot;generate:watch&quot;]<br>    )<br>    .WaitFor(frontend); // 👈 Wait for this because it installs dependencies</pre><h3>Scaffold the application flow</h3><p>The application will flow like this:</p><ol><li>The browser application initiates a connection to a streaming API endpoint to read responses from the agent</li><li>When the user sends a prompt from the browser, it goes into a web API endpoint and then into a System.Threading.Channels.Channel that queues the prompt for the agent; the user can keep sending prompts and they will be queued in the channel</li><li>The agent is an instance of the Copilot harness that is running in a background service connect to the inbound channel to receive the prompt and an outbound channel to stream the responses</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/965/0*OZ8tb4Ej3MZU5475.png" /><figcaption><em>The browser application will call into an API endpoint and then consume the response off a stream.</em></figcaption></figure><p>First, let’s add the .NET SDK for GitHub Copilot:</p><pre>cd src/server<br>dotnet add package GitHub.Copilot.SDK</pre><p>This SDK will allow our backend to operate an instance of the Copilot harness and directly work with any codebase!</p><p>With only a few lines of code, we’re going to build the foundations of an agentic application.</p><p>(As an alternative, you can also use the Agent Framework AgentHarness if you need more control)</p><h3>Add AgentServiceWorker to run the agent in the background</h3><p>The AgentServiceWorker is a background service that will run the agent and handle requests from the API.</p><p>The service initializes the Copilot SDK client and session, and then listens for incoming messages on a channel. When a message is received, it sends the message to the agent and writes the response to an outbound channel.</p><pre>// src/server/AgentServiceWorker.cs<br>using System.Threading.Channels;<br>using GitHub.Copilot;<br>using Microsoft.Extensions.Options;<br><br>namespace Zeeq.Tmpl;<br><br>public class AgentServiceWorker(<br>    IOptions&lt;AppSettings&gt; options,<br>    [FromKeyedServices(&quot;inbound&quot;)] Channel&lt;string&gt; inboundChannel,<br>    [FromKeyedServices(&quot;outbound&quot;)] Channel&lt;string&gt; outboundChannel<br>) : BackgroundService<br>{<br>    // 👇 Change this to some directory on your local disk<br>    private const string WorkingDirectory = &quot;/Users/cchen/code/zeeq/zeeq-app&quot;;<br>    private CopilotClient? _client;<br>    private CopilotSession? _session;<br><br>    <strong>///</strong> <strong>&lt;summary&gt;</strong><br>    <strong>///</strong> 1️⃣  Set up the Copilot client and session with BYOK<br>    <strong>///</strong> <strong>&lt;/summary&gt;</strong><br>    public override async Task StartAsync(CancellationToken cancellationToken)<br>    {<br>        // See: https://github.com/github/copilot-sdk/blob/main/docs/auth/byok.md<br>        // See: https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/recipe/managing-local-files.cs<br>        _client = new(new() { WorkingDirectory = WorkingDirectory });<br>        // 👇 It&#39;s easy to create a session with different models and different capabilities<br>        _session = await _client.CreateSessionAsync(<br>            new()<br>            {<br>                Model = &quot;gpt-5.6-luna&quot;,<br>                OnPermissionRequest = PermissionHandler.ApproveAll,<br>                Provider = new()<br>                {<br>                    Type = &quot;azure&quot;,<br>                    // 👇 This is an Azure OpenAI endpoint; use your own<br>                    BaseUrl = &quot;https://zeeq-open-ai.openai.azure.com&quot;,<br>                    WireApi = &quot;responses&quot;,<br>                    // 👇 Key set in previous step via `dotnet user-secrets set...` (or hardcode here)<br>                    ApiKey = options.Value.LlmApiKey,<br>                },<br>            },<br>            cancellationToken<br>        );<br><br>        await base.StartAsync(cancellationToken);<br>    }<br><br>    <strong>///</strong> <strong>&lt;summary&gt;</strong><br>    <strong>///</strong> 2️⃣  Start the loop that pulls the incoming prompts off of the channel and executes<br>    <strong>///</strong> <strong>&lt;/summary&gt;</strong><br>    protected override async Task ExecuteAsync(CancellationToken stoppingToken)<br>    {<br>        if (_session is null)<br>        {<br>            throw new InvalidOperationException(&quot;Copilot session is not initialized.&quot;);<br>        }<br><br>        if (_client is null)<br>        {<br>            throw new InvalidOperationException(&quot;Copilot client is not initialized.&quot;);<br>        }<br><br>        // Start the streaming output that writes to the outbound channel<br>        _session.On&lt;SessionEvent&gt;(evt =&gt;<br>        {<br>            if (evt is AssistantMessageEvent messageEvent)<br>            {<br>                // 👇 Streams messages out to the SSE endpoint via the channel<br>                outboundChannel.Writer.TryWrite(messageEvent.Data.Content);<br>            }<br>        });<br><br>        // Start the message loop here<br>        while (await inboundChannel.Reader.WaitToReadAsync(stoppingToken))<br>        {<br>            while (inboundChannel.Reader.TryRead(out var message))<br>            {<br>                // 👇 Reads messages from the inbound channel and forwards to the agent<br>                await _session.SendAsync(message, cancellationToken: stoppingToken);<br>            }<br>        }<br>    }<br><br>    <strong>///</strong> <strong>&lt;summary&gt;</strong><br>    <strong>///</strong> 3️⃣  Clean up resources when the service is requested to stop.<br>    <strong>///</strong> <strong>&lt;/summary&gt;</strong><br>    public override async Task StopAsync(CancellationToken cancellationToken)<br>    {<br>        if (_client is not null)<br>        {<br>            await _client.DisposeAsync();<br>        }<br><br>        if (_session is not null)<br>        {<br>            await _session.DisposeAsync();<br>        }<br><br>        await base.StopAsync(cancellationToken);<br>    }<br>}</pre><p>We’re intentionally keeping it simple here, but keep in mind that this <em>is</em> the full SDK for the GitHub Copilot harness so you can really go wild and build some interesting agentic applications on top of this.</p><p>The Copilot SDK supports multiple programming languages (TypeScript/Node, Go, Rust, Java, C#) so it is a solid platform to build agentic applications on top of. It is also likely to have good longevity and stability since it is the same harness that powers GitHub Copilot.</p><p>See more docs here:</p><ul><li><a href="https://github.com/github/copilot-sdk/tree/main">GitHub Copilot SDK</a></li><li><a href="https://github.com/github/copilot-sdk/blob/main/docs/auth/byok.md">BYOK</a></li><li><a href="https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/recipe/multiple-sessions.cs">Multiple sessions</a></li><li><a href="https://github.com/github/awesome-copilot/blob/main/cookbook/copilot-sdk/dotnet/recipe/managing-local-files.cs">Managing local files</a></li></ul><p><a href="https://learn.microsoft.com/en-us/agent-framework/concepts/harness?pivots=programming-language-csharp">Microsoft Agent Framework Agent Harness</a> is a lower-level primitive that may be more suitable for some applications, but requires a bit more work to wire up (exercise left to the reader).</p><h3>Wire it via DI with channels</h3><p>In Program.cs, we can wire up the channels and the AgentServiceWorker:</p><pre>// src/server/Program.cs<br>// Add the channels; keyed for inbound and outbound.<br>builder<br>    .Services.AddKeyedSingleton(<br>        &quot;inbound&quot;,<br>        (sp, key) =&gt; Channel.CreateUnbounded&lt;string&gt;()<br>    )<br>    .AddKeyedSingleton(<br>        &quot;outbound&quot;,<br>        (sp, key) =&gt; Channel.CreateUnbounded&lt;string&gt;()<br>    )<br>    .AddHostedService&lt;AgentServiceWorker&gt;();</pre><p>This is a single-user, local app so using a singleton for the channels makes sense. In a multi-user, stateless, multi-server application, the design would have to be more nuanced and back the endpoints with an actual messaging layer.</p><p>While we’re here, let’s also add the CORS configuration since our frontend and backend are on different ports:</p><pre>// src/server/Program.cs BEFORE the app has been built<br>builder.Services.AddCors(options =&gt;<br>    options.AddDefaultPolicy(policy =&gt;<br>        policy<br>            .AllowAnyOrigin()<br>            .AllowAnyHeader()<br>            .AllowAnyMethod()<br>        )<br>);<br><br>// src/server/Program.cs AFTER the app has been built<br>app.UseCors();</pre><h3>Expose via API endpoints</h3><p>Now we need to wire both the inbound and outbound API endpoints:</p><pre>// src/server/Endpoints/Agent.cs<br>using System.Net.ServerSentEvents;<br>using System.Runtime.CompilerServices;<br>using System.Threading.Channels;<br><br>namespace Zeeq.Tmpl;<br><br>// Inbound endpoint mapping...<br>public class AgentEndpoint : IEndpoint<br>{<br>    public void MapEndpoints(IEndpointRouteBuilder endpoints)<br>    {<br>        endpoints.MapPost(<br>            &quot;/send-prompt&quot;,<br>            (AgentHandler handler, string prompt) =&gt; handler.Handle(prompt)<br>        );<br>    }<br>}<br><br>// ... and the handler<br>public class AgentHandler(<br>    [FromKeyedServices(&quot;inbound&quot;)] Channel&lt;string&gt; inboundChannel<br>) : IEndpointHandler<br>{<br>    public bool Handle(string prompt) =&gt; inboundChannel.Writer.TryWrite(prompt);<br>}<br><br>// Outbound endpoint mapping...<br>public class AgentResponseEndpoint : IEndpoint<br>{<br>    public void MapEndpoints(IEndpointRouteBuilder endpoints)<br>    {<br>        endpoints.MapGet(<br>            &quot;/read-response&quot;,<br>            async (AgentResponseHandler handler, CancellationToken cancellation) =&gt;<br>                TypedResults.ServerSentEvents(handler.HandleAsync(cancellation))<br>                // 👆 The response type is SSE; this translates at the OpenAPI spec!<br>        );<br>    }<br>}<br><br>// ...and the streaming handler<br>public class AgentResponseHandler(<br>    [FromKeyedServices(&quot;outbound&quot;)] Channel&lt;string&gt; outboundChannel<br>) : IEndpointHandler<br>{<br>    // 👇 Note the typed `SseItem&lt;T&gt;`; in C# 15, this is even nicer with unions<br>    public async IAsyncEnumerable&lt;SseItem&lt;string&gt;&gt; HandleAsync(<br>        [EnumeratorCancellation] CancellationToken cancellation<br>    )<br>    {<br>        while (await outboundChannel.Reader.WaitToReadAsync(cancellation))<br>        {<br>            while (outboundChannel.Reader.TryRead(out var response))<br>            {<br>                yield return new SseItem&lt;string&gt;(response);<br>            }<br>        }<br>    }<br>}</pre><p>Here, you might wonder why the separation of the HTTP endpoint from the handler since the code is quite simple.</p><p>This is an <em>intentional pattern</em> because the handler is injected into the DI container. This means that the application can attach authentication and input validation (including deserialization) at the app endpoint, but the agent can use CSharpRepl to directly manipulate the handler <strong><em>without going through the HTTP layer</em></strong>.</p><p>Note how nicely everything is typed. When the client is generated via Kubb, the type information flows through to the frontend as well.</p><p>In the upcoming C# 15 release, this story gets even better with union types which map cleanly at the boundary and translate directly into TypeScript union types.</p><h3>Building a simple frontend</h3><p>The frontend in its current state is cloned from the Nuxt UI template.</p><p>Some adjustments are needed to build a simple chat interface to the agent.</p><h3>Add packages for Markdown and code rendering</h3><p>We’ll need two packages to help with the display of Markdown and code snippets</p><pre>cd src/app<br>pnpm add @comark/vue shiki</pre><p>A small update to the vite.config.ts is needed to add the @comark/vue plugin:</p><pre>// src/app/vite.config.ts<br>export default defineConfig({<br>  plugins: [<br>    vueRouter({<br>      dts: &#39;src/route-map.d.ts&#39;<br>    }),<br>    vue(),<br>    ui({<br>      prose: true, // 👈 Need to add this (see the Nuxt UI docs)<br>      ui: {<br>        colors: {<br>          primary: &#39;green&#39;,<br>          neutral: &#39;zinc&#39;<br>        }<br>      }<br>    })<br>  ]<br>})</pre><p>You can learn more about <a href="https://ui.nuxt.com/docs/typography">Nuxt UI Markdown rendering and typography in the docs</a>.</p><h3>Implement the chat agent composable</h3><p>Building the backend produces an updated OpenAPI schema which the Kubb watcher should pick up and create new stubs.</p><p>We can use these stubs in a Vue composable to implement the interface to the backend API.</p><pre>import { ref } from &#39;vue&#39;<br>// 👇 Import the API clients from the Kubb generated output<br>import { postSendPrompt } from &#39;../api/generated/clients/postSendPrompt&#39;<br>import { getReadResponse } from &#39;../api/generated/clients/getReadResponse&#39;<br><br>export type ChatStatus = &#39;ready&#39; | &#39;submitted&#39; | &#39;streaming&#39; | &#39;error&#39;<br><br>export interface ChatTextPart {<br>  type: &#39;text&#39;<br>  id: string<br>  text: string<br>}<br><br>export interface ChatMessage {<br>  id: string<br>  role: &#39;user&#39; | &#39;assistant&#39;<br>  parts: ChatTextPart[]<br>}<br><br>/** No explicit end-of-turn signal comes from the server, so a response is<br> * considered finished once no new chunk has arrived for this long. */<br>const READY_DELAY_MS = 800<br><br>export function useAgentChat() {<br>  const messages = ref&lt;ChatMessage[]&gt;([])<br>  const status = ref&lt;ChatStatus&gt;(&#39;ready&#39;)<br><br>  let readyTimeout: ReturnType&lt;typeof setTimeout&gt; | undefined<br>  let assistantMessage: ChatMessage | undefined<br>  let responseStreamStarted = false<br><br>  function scheduleReady() {<br>    clearTimeout(readyTimeout)<br>    readyTimeout = setTimeout(() =&gt; {<br>      status.value = &#39;ready&#39;<br>      assistantMessage = undefined<br>    }, READY_DELAY_MS)<br>  }<br><br>  async function consumeResponseStream() {<br>    const { stream } = await getReadResponse()<br><br>    for await (const event of stream) {<br>      const chunk = typeof event.data === &#39;string&#39; ? event.data : &#39;&#39;<br>      if (!chunk) continue<br><br>      if (!assistantMessage) {<br>        assistantMessage = {<br>          id: crypto.randomUUID(),<br>          role: &#39;assistant&#39;,<br>          parts: [{ type: &#39;text&#39;, id: crypto.randomUUID(), text: &#39;&#39; }],<br>        }<br>        messages.value.push(assistantMessage)<br>      }<br><br>      assistantMessage.parts[0].text += chunk<br>      status.value = &#39;streaming&#39;<br>      scheduleReady()<br>    }<br>  }<br><br>  async function sendPrompt(prompt: string) {<br>    const trimmed = prompt.trim()<br>    if (!trimmed || status.value === &#39;submitted&#39; || status.value === &#39;streaming&#39;) return<br><br>    messages.value.push({<br>      id: crypto.randomUUID(),<br>      role: &#39;user&#39;,<br>      parts: [{ type: &#39;text&#39;, id: crypto.randomUUID(), text: trimmed }],<br>    })<br><br>    status.value = &#39;submitted&#39;<br>    assistantMessage = undefined<br><br>    if (!responseStreamStarted) {<br>      responseStreamStarted = true<br>      consumeResponseStream().catch(() =&gt; {<br>        status.value = &#39;error&#39;<br>      })<br>    }<br><br>    try {<br>      await postSendPrompt({ query: { prompt: trimmed } })<br>    } catch {<br>      status.value = &#39;error&#39;<br>    }<br>  }<br><br>  return { messages, status, sendPrompt }<br>}</pre><h3>Use the composable in the UI</h3><p>And now we update the UI to implement a simple chat interface once again using Nuxt UI components:</p><pre>&lt;template&gt;<br>  &lt;div class=&quot;flex h-full justify-center p-4&quot;&gt;<br>    &lt;UCard<br>      class=&quot;flex h-full w-full max-w-md flex-col&quot;<br>      :ui=&quot;{ body: &#39;flex-1 min-h-0 overflow-y-auto&#39;, footer: &#39;shrink-0&#39; }&quot;<br>    &gt;<br>      &lt;UChatMessages<br>        :messages=&quot;messages&quot;<br>        :status=&quot;status&quot;<br>      &gt;<br>        &lt;template #content=&quot;{ message }&quot;&gt;<br>          &lt;template<br>            v-for=&quot;part in message.parts&quot;<br>            :key=&quot;part.id&quot;<br>          &gt;<br>            &lt;Markdown<br>              v-if=&quot;part.type === &#39;text&#39;&quot;<br>              :value=&quot;part.text&quot;<br>              :plugins=&quot;[shiki()]&quot;<br>              :streaming=&quot;message.role === &#39;assistant&#39; &amp;&amp; status === &#39;streaming&#39;&quot;<br>              unwrap<br>            /&gt;<br>          &lt;/template&gt;<br>        &lt;/template&gt;<br>      &lt;/UChatMessages&gt;<br><br>      &lt;template #footer&gt;<br>        &lt;UChatPrompt<br>          v-model=&quot;input&quot;<br>          :status=&quot;status&quot;<br>          @submit=&quot;onSubmit&quot;<br>        &gt;<br>          &lt;UChatPromptSubmit :status=&quot;status&quot; /&gt;<br>        &lt;/UChatPrompt&gt;<br>      &lt;/template&gt;<br>    &lt;/UCard&gt;<br>  &lt;/div&gt;<br>&lt;/template&gt;<br><br>&lt;!-- Medium doesn&#39;t allow script tag?? --&gt;<br>&lt;_script setup lang=&quot;ts&quot;&gt;<br>import { ref } from &#39;vue&#39;<br>import { Markdown } from &#39;@comark/vue&#39;<br>import shiki from &#39;@comark/vue/plugins/shiki&#39;<br>import { useAgentChat } from &#39;../composables/useAgentChat&#39;<br><br>const input = ref(&#39;&#39;)<br>const { messages, status, sendPrompt } = useAgentChat()<br><br>function on_Submit() {<br>  const prompt = input.value<br>  input.value = &#39;&#39;<br>  sendPrompt(prompt)<br>}<br>&lt;/_script&gt;</pre><p>Here, we can see Nuxt UI paying huge dividends by providing not only a very rich, comprehensive set of components, but also agent-friendly docs to build rapidly.</p><p>Nuxt UI includes a nice set of components for this, out of the box:</p><ul><li><a href="https://ui.nuxt.com/docs/components/chat-messages">UChatMessages</a></li><li><a href="https://ui.nuxt.com/docs/components/chat-prompt">UChatPrompt</a></li></ul><p>With plenty more to build a fully-featured agent interface.</p><h3>Adding a skill for CSharpRepl</h3><p>Agents <em>can</em> figure out CSharpRepl by themselves since the CLI itself has good instructions, but it will help A LOT if we provide the agents examples of how to work with CSharpRepl. We can effectively capture the key scenarios that the agents need to know about by walking the agent through how we want it to use the CSharpRepl CLI.</p><p>Rather than provide the finished skill, I’ll share the prompts that you can use to get the agent to build the skill.</p><h3>Create the skill directory and placeholder file</h3><p>Different harnesses will acquire local skills in different ways (for now) so we will need to do some basic scaffolding of the skill for different harnesses.</p><pre># Add the .agents directory which most harnesses will use<br>mkdir .agents<br>mkdir .agents/skills<br>mkdir .agents/skills/csharprepl<br>touch .agents/skills/csharprepl/SKILL.md<br><br># Add the .claude directory and symlink the skills<br>mkdir .claude<br>cd .claude<br>ln -s ../.agents/skills skills<br><br># Codex should already be present; symlink the skills<br>cd .codex<br>ln -s ../.agents/skills skills</pre><p>This should cover all mainstream harnesses.</p><h3>Prompt the agent to write the key instructions for the skill</h3><p>We’ll use the agent to build the skill directly by having the agent run the CLI.</p><p>Your results may vary by harness, model, and your prompts so here’s my guide to help the agent discover and build the key elements of the skill.</p><p><em>(Note: best to paste each prompt one-by-one to see the agent iterate on building the skill)</em></p><pre>&lt;!-- Prime the agent so it knows what we&#39;re doing --&gt;<br>We&#39;re working on writing a skill in .agents/skills/csharprepl/SKILL.md for the CSharpRepl CLI.<br>CSharpRepl is a REPL that allows agents to execute C# code in a running .NET application.<br>Our goal is to keep this skill short, terse, to the point with key examples broken down into sections.<br>Each section should have one code block that demonstrates the different commands relevant to that section.<br>Use comments above the command in the fenced code block to explain the command and use case.<br>Keep writing dense and terse; to the point and instructional with notes for key operational details.<br>We&#39;ll do this iteratively and refactor this document as we learn.<br>Start by running `csharprepl connect -h` to learn the commands.<br>Write a quick introductory section and then a table of the key basic commands.<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Now let&#39;s ask it to get an instance of the health service and replace it --&gt;<br>Read: https://fuqua.io/blog/2026/06/injecting-a-csharp-repl-into-a-running-net-process/<br>Read: https://github.com/waf/CSharpRepl/wiki/Connecting-to-a-Running-Process<br>Acquire an instance of the `HealthHandler` and use `#replace` to replace it with a different implementation.<br>&lt;example&gt;<br>printf &#39;%s\n&#39; \<br>  &#39;#replace Zeeq.Tmpl.HealthHandler.Handle with (instance) =&gt; &quot;Healthy (replaced!)&quot;&#39; \<br>  &#39;var health = Get&lt;Zeeq.Tmpl.HealthHandler&gt;(); var msg = health.Handle(); msg&#39; \<br>  | NO_COLOR=1 csharprepl connect 92260 --streamPipedInput<br>&lt;example&gt;<br>Use `aspire logs` to see the output and verify that the replace worked.<br>Document your findings and how to use #replace in the skill file.<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Now let&#39;s ask it to get an instance of the health service and wrap it --&gt;<br>Read: https://fuqua.io/blog/2026/06/injecting-a-csharp-repl-into-a-running-net-process/<br>Read: https://github.com/waf/CSharpRepl/wiki/Connecting-to-a-Running-Process<br>Acquire an instance of the `HealthHandler` and use `#wrap` to wrap it with a console log.<br>Use `aspire logs` to see the output and verify that the wrap worked.<br>Document your findings and how to use #wrap in the skill file.<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- We also want to prompt it to learn how to use .csx files for more complex ops--&gt;<br>Now do both exercises again using `--eval-file` and write a `.csx` file to perform the same action.<br>Update and document any findings necessary in the file<br>When done, I&#39;ll prompt the next experiment<br><br>&lt;!-- Tighten up the notes to make it more dense --&gt;<br>Write notes more dense<br>Compact but no loss of clarity<br>Keep key details only<br>Terse notes and comments<br><br>&lt;!-- Experiment with the `-u` flag to make code more terse--&gt;<br>Test -u --using &lt;namespace&gt; and document<br>This makes scripts more terse (no namespace inline)<br>Update and document any findings necessary in the file</pre><p>By providing the agent reference docs and a few examples with clear instructions for discovery, the agent is able to self-materialize the skill with the key information we need it to have.</p><h3>Closing thoughts</h3><p>At this point, we have a <strong><em>solid</em></strong> foundation for building an agentic application on top of Aspire using .NET, C#, the Copilot SDK, and a Vue + Nuxt UI frontend.</p><p>This set of exercises moves very intentionally so human operators can have a clear view of what’s happening in the code and we’ve made very specific design decisions on how to wire up our endpoints to make it more friendly for agents to work with.</p><p>The setup is now built for coding agents to work with the codebase and iterate on the application itself.</p><p>In the next part of this series (Part 4), we’ll wire up the the data model as well as Testcontainers for isolated, transactional integration testing to let agents move autonomously as they build and verify correctness. Then in the final part, we’ll wire up logging, telemetry, and observability before building the final prototype specification writing application that will use the Copilot SDK to write a technical specification as we type out modify the business requirements.</p><p>We’ll still move with purpose so that each technical decision is clear and explained in the context of building an agentic foundation for an application.</p><blockquote><em>If you are curious to see a real-world setup, check out the </em><a href="https://zeeq.ai/"><em>Zeeq.ai</em></a><em> repo: </em><a href="https://github.com/zeeq-ai/zeeq-app"><em>https://github.com/zeeq-ai/zeeq-app</em></a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fba953e01143" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/the-unexpected-ai-stack-c-net-part-3-fba953e01143">The Unexpected AI Stack: C# + .NET (Part 3)</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Unexpected AI Stack: C# + .NET (Part 2)]]></title>
            <link>https://itnext.io/the-unexpected-ai-stack-c-net-part-2-2dddd1e36fab?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/2dddd1e36fab</guid>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Mon, 17 Aug 2026 17:33:11 GMT</pubDate>
            <atom:updated>2026-08-17T19:26:54.499Z</atom:updated>
            <content:encoded><![CDATA[<h3>The Unexpected AI Stack: C# + .NET (Part 2)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*CYs0sUcNJG_ckiWS.png" /><figcaption>Building the foundations of an agent-friendly codebase using .NET, C#, and Aspire</figcaption></figure><h3>Summary</h3><ul><li>This exercise will set up the foundational scaffolding for a typical Vite frontend app connected to a standalone backend via OpenAPI. The objective is to focus on the DX and getting key pieces in place so that agents a team can operate on top of this codebase and iterate rapidly with agents.</li><li>Mise is used to simplify the installation of required tooling and the runtime environment for development</li><li>Aspire is used to orchestrate the runtime components of the application</li><li>We’ll also wire up CSharpRepl and give it a test run, but there’s not much for it to do yet!</li></ul><h3>What we’re building</h3><p>This exercise will build a chat frontend application connected to an instance of a Copilot agent via the <a href="https://github.com/github/copilot-sdk">Copilot SDK</a>.</p><p>The Copilot SDK is a solid choice for building agentic applications because:</p><ul><li>The SDK supports multiple language bindings including TypeScript, Python, Go, C#, Rust, and Java</li><li>The harness is model agnostic and allows using different models for different tasks and BYOK or use the GitHub Copilot subscription (we’ll be using BYOK for this exercise)</li><li>The SDK is well documented and comes with advanced features for working with and navigating source code.</li></ul><p>For the front-end, we’ll build a chat interface using a Vite + Nuxt UI web app using a streaming API. We’ll use <a href="https://kubb.dev/">Kubb.dev</a> for client generation from OpenAPI specs.</p><p>Part 3 will focus more on the build out; here, we’re setting up the foundation first.</p><h3>Scaffolding the runtime configuration</h3><p>The runtime will require configuring Mise to install the tooling that we’ll need. This makes it easier for teams to onboard new developers by simplifying the installation process and ensuring developers have the right versions of tools installed with minimal friction.</p><h3>Configuring Mise</h3><p><a href="https://mise.jdx.dev/installing-mise.html">Install Mise for your platform following the instructions</a></p><pre># Create the config directory and place the mise.toml file there<br>mkdir .config<br>cd .config<br>touch mise.toml</pre><p>The config file needs to specify our dependencies. The easiest way to do this from the command line is to type mise use which has auto-complete for popular packages:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/0*ybjoS21Yxp-e5PL5.gif" /><figcaption>Mise makes it easy to configure required stack dependencies</figcaption></figure><p>This will add entries into the mise.toml file like this:</p><pre>[tools]<br>dotnet = &quot;10.0.302&quot;</pre><p>To add tools:</p><pre># After the @, the versions will resolve from Nuget<br>mise use dotnet:Aspire.Cli@</pre><p>Here’s the full file of the tools we’ll need:</p><pre>[tools]<br>dotnet = &quot;10.0.302&quot;<br>&quot;dotnet:Aspire.Cli&quot; = &quot;13.4.6&quot;<br>&quot;dotnet:CsharpRepl&quot; = &quot;0.9.2&quot;<br>&quot;dotnet:Csharpier&quot; = &quot;1.3.0&quot;<br>&quot;dotnet:dotnet-ef&quot; = &quot;10.0.11&quot;<br>node = &quot;26&quot;<br>pnpm = &quot;11.18.0&quot;</pre><p>(Update the versions as necessary)</p><p>When pulling the stack, run mise install to configure a local environment with the correct versions of the specified tools.</p><h3>Wire up AGENTS.md and CLAUDE.md as a symlink</h3><pre># At the root, create the AGENTS.md file and symlink it to CLAUDE.md<br>touch AGENTS.md<br>ln -s AGENTS.md CLAUDE.md</pre><p>Here’s the simple AGENTS.md we’ll use to describe how the agent should work in this repo:</p><pre># .NET 10, C# 14 Web API with Nuxt UI and Vite Frontend<br><br>- `src/server`: .NET 10, C# 14; use modern language features (primary constructors, deconstruction, pattern matching, switch expressions, named tuple types, etc.)<br>- `src/app`: Vue 3, Nuxt UI, Vite, TypeScript, Tailwind CSS<br><br>## Tooling<br><br>### Nuxt UI Rules<br><br>- Prefer Nuxt UI components first before writing new components<br>- Use Nuxt UI **component slots** first when suitable<br>- Use Nuxt UI **component props** before writing custom layout or style<br>- &lt;https://ui.nuxt.com/llms.txt&gt; has dense listing of components available to check<br>- &lt;https://ui.nuxt.com/llms-full.txt&gt; (download and search)<br><br>### Icons, Text, Tailwind<br><br>- Use `lucide` for Nuxt UI (`i-lucide-*`)<br><br>### Aspire CLI<br><br>```shell<br>aspire -h<br><br># Get resources if needed; but usually just use the rebuild command below<br>aspire ps<br><br># Avoid restarting the full stack; just rebuild the server<br>aspire resource app-backend rebuild<br>```<br><br>### Playwright MCP<br><br>Use the Playwright MCP to run the app in a browser and test the UI.<br><br>For UI work, get access first thing **before** you start!</pre><h3>Set up the Nuxt UI and Playwright MCP</h3><p>The Playwright MCP will give the agent access to operate on the UI to perform UI layer E2E testing.</p><pre># Nuxt UI<br>claude mcp add --scope project --transport http nuxt-ui-remote https://ui.nuxt.com/mcp<br><br># Playwright<br>claude mcp add --scope project playwright -- npx -y @playwright/mcp@latest --extension</pre><p>This creates the following .mcp.json</p><pre>{<br>  &quot;mcpServers&quot;: {<br>    &quot;playwright&quot;: {<br>      &quot;type&quot;: &quot;stdio&quot;,<br>      &quot;command&quot;: &quot;npx&quot;,<br>      &quot;args&quot;: [<br>        &quot;-y&quot;,<br>        &quot;@playwright/mcp@latest&quot;,<br>        &quot;--extension&quot;<br>      ],<br>      &quot;env&quot;: {}<br>    },<br>    &quot;nuxt-ui-remote&quot;: {<br>      &quot;type&quot;: &quot;http&quot;,<br>      &quot;url&quot;: &quot;https://ui.nuxt.com/mcp&quot;<br>    }<br>  }<br>}</pre><p>Codex/ChatGPT doesn’t (yet?) have a project local CLI command for MCP so we’ll manually create .codex/config.toml:</p><pre>[mcp_servers.nuxt-ui-remote]<br>enabled = true<br>url = https://ui.nuxt.com/mcp&quot;<br><br>[mcp_servers.playwright]<br>command = &quot;npx&quot;<br>args = [&quot;@playwright/mcp@latest&quot;, &quot;--extension&quot;]</pre><p>Install <a href="https://chromewebstore.google.com/detail/playwright-extension/mmlmfjhmonkocbjadbfplnigmagldckm">the Chrome extension for Playwright MCP</a>. This extension is useful because it allows the agent to drive an existing browser session so you can manually log in and bypass the complexity of auth flows.</p><p>In practice, with CSharpRepl, Playwright becomes only necessary to address UI layout since the agent can directly interface with the application at the API interface layer for E2E integration testing (bypassing the need to perform auth or use slower UI interactions altogether). But applications that implement auth will still benefit from using Playwright with the extension when iterating on the UI.</p><h3>Setting up the applications</h3><h3>Scaffold the backend minimal web API</h3><p>For this application, we’re going to use a slightly tweaked minimal web API that I think makes it easier for agents to work with.</p><pre>mkdir src<br>mkdir src/server<br>cd src/server<br>dotnet new webapi --no-https<br>dotnet user-secrets init<br>dotnet user-secrets set AppSettings:LlmApiKey &quot;YOUR_API_KEY_HERE&quot;<br>cd ../../<br>dotnet new sln              # Add solution file<br>dotnet sln add src/server   # Add the project to the solution<br>dotnet build                # First build</pre><p>At this point, the server can already run, but it’s only serving the default weather forecast endpoint.</p><p>We’re going to add two helper interfaces: IEndpoint and IEndpointHandler to make it easier to parcel out the endpoint implementations into separate files. We’ll also add the configuration object as well.</p><pre>// src/server/IEndpoint.cs<br>public interface IEndpoint<br>{<br>    void MapEndpoints(IEndpointRouteBuilder endpoints);<br>}<br><br>// src/server/IEndpointHandler.cs<br>public interface IEndpointHandler { }<br><br>// src/server/AppSettings.cs<br>public record AppSettings<br>{<br>    public string LlmApiKey { get; init; } =<br>        &quot;dotnet user-secrets set AppSettings:LlmApiKey YOUR_API_KEY_HERE&quot;;<br>}</pre><p>We’ll add the <a href="https://www.nuget.org/packages/Scrutor">Scrutor package</a> to make the wiring a bit more terse:</p><pre>cd src/server<br>dotnet add package Scrutor</pre><p>Then in src/server/Program.cs, replace with:</p><pre>var builder = WebApplication.CreateBuilder(args);<br><br>// Add the settings into the DI container<br>builder<br>    .Services.AddOptions&lt;AppSettings&gt;()<br>    .Bind(builder.Configuration.GetSection(nameof(AppSettings)))<br>    .ValidateOnStart();<br><br>// Wire up endpoints into the DI container<br>builder<br>    .Services.AddOpenApi()<br>    .Scan(scan =&gt;<br>        scan.FromApplicationDependencies()<br>            .AddClasses(classes =&gt; classes.AssignableTo&lt;IEndpoint&gt;())<br>            .AsImplementedInterfaces()<br>            .WithTransientLifetime()<br>            .AddClasses(classes =&gt; classes.AssignableTo&lt;IEndpointHandler&gt;())<br>            .AsSelf()<br>            .WithTransientLifetime()<br>    );<br><br>var app = builder.Build();<br><br>if (app.Environment.IsDevelopment())<br>{<br>    app.MapOpenApi();<br>}<br><br>// Connect the endpoints<br>var endpoints = app.Services.GetRequiredService&lt;IEnumerable&lt;IEndpoint&gt;&gt;();<br><br>foreach (var endpoint in endpoints)<br>{<br>    endpoint.MapEndpoints(app);<br>}<br><br>app.Run();</pre><p>To test this, we’ll add a simple health check endpoint and handler.</p><pre>// src/server/Health.cs<br>public class HealthEndpoint : IEndpoint<br>{<br>    public void MapEndpoints(IEndpointRouteBuilder endpoints)<br>    {<br>        // 👇 DI at the endpoint to separate the HTTP binding from the app code<br>        endpoints.MapGet(&quot;/health&quot;, (HealthHandler handler) =&gt; handler.Handle());<br>    }<br>}<br><br>public class HealthHandler : IEndpointHandler<br>{<br>    // 👇 This simple example looks like overkill, but scales well!<br>    public string Handle() =&gt; $&quot;Healthy @ {DateTime.UtcNow}&quot;;<br>}</pre><p>This design is intentional even though the health handler is simple enough to implement directly inline with the MapGet. By injecting handlers as a dependency, we’ll be able to directly manipulate it at runtime later using CSharpRepl. While this example is simple, a key ingredient to successful agentic engineering is <strong><em>consistency</em></strong>. So a goal here is to <em>establish the pattern that the agent will follow</em> when building the next set of handlers.</p><p>In practice, I generally create separate files for the endpoint and the handler so the agents follow the pattern.</p><p>An important note with minimal API endpoints is that it is possible to directly access the user as a DI injected parameter:</p><pre>// src/server/SomeAuthenticatedEndpoint.cs<br>public class SomeAuthenticatedEndpoint : IEndpoint<br>{<br>    public void MapEndpoints(IEndpointRouteBuilder endpoints)<br>    {<br>        // 👇 DI at the endpoint to separate the HTTP binding from the app code<br>        endpoints.MapGet(<br>            &quot;/some-authenticated-endpoint&quot;,<br>            (<br>                ClaimsPrincipal user, // 👈 DI will resolve and inject this<br>                SomeAuthenticatedHandler handler<br>            ) =&gt; handler.Handle(user) // 👈 Pass forward to the handler<br>        );<br>    }<br>}</pre><p>This distinction is very useful since it means when CSharpRepl is connected, it can easily simulate different users by pulling out the injected handler and directly injecting a different ClaimsPrincipal to simulate different users and test the behavior of the handler <em>without going through auth</em>.</p><p>Now we can quickly run and curl this as a smoke test:</p><pre># Run the server<br>dotnet run<br><br># Check the endpoint<br>curl http://localhost:5138 -v # Healthy @ 8/11/2026 8:46:50 PM</pre><h3>Scaffold the frontend Nuxt UI Vite app</h3><p>The Nuxt UI Vite app setup mostly follows this doc: <a href="https://ui.nuxt.com/docs/getting-started/installation/vue">https://ui.nuxt.com/docs/getting-started/installation/vue</a></p><p>We’ll use the starter Vue + Vite template: <a href="https://github.com/nuxt-ui-templates/starter-vue">https://github.com/nuxt-ui-templates/starter-vue</a></p><pre>mkdir src/app<br>cd src/app<br>git clone https://github.com/nuxt-ui-templates/starter-vue.git .<br># Remove the .git directory and .github directory since we don&#39;t need these<br>rm -rf .git .github</pre><p>This gives us a basic starter template (<a href="https://starter-vue-template.nuxt.dev/">see the template</a>) so we can skip the manual wiring of Nuxt UI itself.</p><pre>cd src/app<br>pnpm dev # App served at http://localhost:5173/</pre><h3>Wiring into Aspire</h3><p>Now that we have both the backend and frontend scaffolded, we can wire them into Aspire so that one command orchestrates the stack (albeit a simple stack at the moment!).</p><h3>Create the Aspire host</h3><p>Because the application has .slnx file at the root, Aspire will create a project-based setup:</p><pre># From root<br>aspire init --language csharp<br><br># Rename for convenience<br>mv zeeq-tmpl.AppHost host<br>mv host/zeeq-tmpl.AppHost.csproj host/host.csproj<br><br># Add to the solution and add a reference to the server project<br>dotnet sln add host<br>dotnet add reference --project host src/server<br><br># Add the JavaScript module that will give us pnpm and Vite support<br>aspire add # Then type and select &quot;JavaScript&quot;</pre><p>At this point, you can use <a href="https://aspire.dev/reference/cli/commands/aspire-init/#the-aspireify-skill">the </a><a href="https://aspire.dev/reference/cli/commands/aspire-init/#the-aspireify-skill">aspireify skill</a> to wire up the app, but it’s also instructive to set it up manually.</p><h3>Add the Aspire resources</h3><p>Now we can add the two application resources to the host.</p><pre>// host/AppHost.cs<br>var builder = DistributedApplication.CreateBuilder(args);<br><br>var backend = builder.AddProject&lt;Projects.server&gt;(&quot;app-backend&quot;);<br><br>var frontend = builder<br>    .AddViteApp(name: &quot;zeeq-planner&quot;, appDirectory: &quot;../src/app&quot;)<br>    .WithPnpm()<br>    // 👇 Note: I fixed a port here to make it easier to find<br>    .WithEndpoint(name: &quot;http&quot;, port: 7321, scheme: &quot;http&quot;)<br>    .WaitFor(backend);<br>builder.Build().Run();</pre><p>The stack is ready to run 🚀:</p><pre># Start the stack<br>➜  zeeq-tmpl aspire run --detach --non-interactive<br>Finding AppHosts...<br>host/host.csproj<br>🛑 Stopping previous instance (AppHost PID: 85558, CLI PID: 85469)<br>✅ Running instance stopped successfully.<br>Starting Aspire AppHost in the background...<br><br>AppHost:  host/host.csproj<br><br>   Dashboard:  https://localhost:17245/login?t=8555e27c92c928953486d41d2a3d0db8<br>        Logs:  /Users/cchen/.aspire/logs/cli_20260811T211920737_detach-child_7987c3fad30b<br>               4bc88b08ced7272136b8.log<br>         PID:  87818<br><br>✅ AppHost started successfully.<br><br># Stop the stack<br>aspire stop</pre><p>This same pattern can be used to wire up Postgres, Redis, Kafka, and many other modules that Aspire supports which makes it easy to build full-stack, distributed applications that start up with a single command and provides agents with the right interfaces to rapidly iterate on the application.</p><h3>Setting up CSharpRepl and a quick smoke test</h3><p>CSharpRepl’s wiring requires running csharprepl connect init to list the location of the CSharpRepl installation. To automate this, we’ll need to run the command and parse it out of the output.</p><pre>using System.Diagnostics;<br><br>var builder = DistributedApplication.CreateBuilder(args);<br><br>var csrHook = ResolveCSharpReplHook(); // 👈 Extract the local hook path<br><br>var backend = builder<br>    .AddProject&lt;Projects.server&gt;(&quot;app-backend&quot;)<br>    // 👇 Wire up CSharpRepl environment variables to the runtime.<br>    .WithEnvironment(&quot;DOTNET_STARTUP_HOOKS&quot;, csrHook)<br>    .WithEnvironment(&quot;ASPNETCORE_HOSTINGSTARTUPASSEMBLIES&quot;, &quot;CSharpRepl.InjectedHook&quot;);<br><br>var frontend = builder<br>    .AddViteApp(name: &quot;app-frontend&quot;, appDirectory: &quot;../src/app&quot;)<br>    .WithPnpm()<br>    .WithEndpoint(name: &quot;http&quot;, port: 7321, scheme: &quot;http&quot;)<br>    .WaitFor(backend);<br><br>builder.Build().Run();<br><br>// ⭐️ A helper function that runs `csharprepl connect init` and extracts the path to<br>// the hook .dll from the output.<br>string ResolveCSharpReplHook()<br>{<br>    var process =<br>        Process.Start(<br>            new ProcessStartInfo<br>            {<br>                FileName = &quot;csharprepl&quot;,<br>                Arguments = &quot;connect init&quot;,<br>                RedirectStandardOutput = true,<br>                UseShellExecute = false,<br>                CreateNoWindow = true,<br>            }<br>        ) ?? throw new InvalidOperationException(&quot;Failed to start csharprepl process.&quot;);<br><br>    // Full output from the command; need to extract just the hook .dll<br>    return process<br>        .StandardOutput.ReadToEnd()<br>        .Split(&#39;\n&#39;)<br>        .Select(line =&gt; line.Trim())<br>        .First(line =&gt; line.StartsWith(&quot;export DOTNET_STARTUP_HOOKS=&quot;))<br>        .Split(&#39;&quot;&#39;)[1];<br>}</pre><p>csharprepl connect list will show the running processes:</p><pre>➜  zeeq-tmpl git:(main) ✗ csharprepl connect list<br><br>  PID   │ Process<br> ───────┼─────────────────────<br>  71304 │ dotnet<br>  71441 │ dotnet<br>  92260 │ server</pre><p>And we can connect directly to the repl with csharprepl connect &lt;PID&gt; and manipulate the runtime application.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/896/0*qEDO8UOsQwtOvfOZ.gif" /></figure><p>Coding agents will be able to use the same REPL via --eval &lt;code&gt; or --eval-file &lt;file&gt; to manipulate the runtime state of the application.</p><p>Here’s an example of replace the health endpoint to change the logging behavior:</p><pre>printf &#39;%s\n&#39; \<br>  &#39;#replace Zeeq.Tmpl.HealthHandler.Handle with (instance) =&gt; &quot;Healthy (replaced!)&quot;&#39; \<br>  &#39;var health = Get&lt;Zeeq.Tmpl.HealthHandler&gt;(); var msg = health.Handle(); msg&#39; \<br>  | NO_COLOR=1 csharprepl connect 92260 --streamPipedInput</pre><p>What I hope is impressed here is that with CSharpRepl connected, agents can directly work with the fully wired up DI container and effectively simulate failure modes more realistically than possible with only integration tests. This gives agents a powerful tool to iterate and diagnose, build, or perform E2E regression tests in more realistic scenarios.</p><h3>Closing thoughts</h3><p>In part 2 of examining how to use C#, .NET, and Aspire to build agent-friendly applications, we’ve set up the foundation to build and extend the application.</p><p>What should be evident at this stage is that Aspire and CSharpRepl provide a powerful combination of tools that allow agents to operate autonomously and iterate on an application rapidly. This same scaffolding will make it easier to reproduce issues, reproduce production failure modes, check for regressions, and experiment with new features rapidly.</p><p>In the next section, we’ll:</p><ul><li>Add a skill to help agents understand how to effectively use CSharpRepl</li><li>Wire up the API client generation from the backend to the frontend</li><li>Start to incorporate the Copilot SDK and build a simple streaming chat interface to the agent now that the foundation of the application is ready.</li></ul><blockquote><em>If you are curious to see a real-world setup, check out the </em><a href="https://zeeq.ai/"><em>Zeeq.ai</em></a><em> repo: </em><a href="https://github.com/zeeq-ai/zeeq-app"><em>https://github.com/zeeq-ai/zeeq-app</em></a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2dddd1e36fab" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/the-unexpected-ai-stack-c-net-part-2-2dddd1e36fab">The Unexpected AI Stack: C# + .NET (Part 2)</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Unexpected AI Stack: C# + .NET (Part 1)]]></title>
            <link>https://itnext.io/the-unexpected-ai-stack-c-net-part-1-5a45f46a1ff1?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/5a45f46a1ff1</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Thu, 13 Aug 2026 13:10:14 GMT</pubDate>
            <atom:updated>2026-08-13T19:41:19.722Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PNjBX7g4TniX2xjMb0I3jw.png" /><figcaption>A surprisingly powerful stack for building AI-enabled, agent-friendly applications.</figcaption></figure><h3>Summary</h3><ul><li>C# and .NET as a stack are probably flying under the radar for many teams building new apps in the agentic era</li><li>For teams already using legacy C# and .NET, it may not be clear how to best leverage the modern .NET stack tooling to speed up agentic development</li><li>Aspire tooling provides agents a programmable, isolated runtime orchestration layer that improves agent autonomy and facilitates agentic patterns like worktrees and parallel development.</li><li>CSharpRepl lets agents interact with an instance of <em>the running application</em> and manipulate the runtime state of the application including wrapping and replacing existing functions.</li><li>Combined, this set of tooling gives agents more autonomy to iterate and build stable, correct, production-ready code that is proven in <em>runtime configurations</em> efficiently.</li></ul><h3>Why C#?</h3><p>In an age when agents can write code in any programming language, it’s fair to ask: why have agents write code in C#?</p><p>Last year, I was a technical lead on the effort at Motion ($500m valuation, series C, post YCombinator startup, ~40 engineers) <a href="https://engineering.usemotion.com/moving-off-of-typescript-e7bb1f3ad091"><strong>to move the entire backend off of TypeScript + Node and shift to a combination of C# and F# for all future backends</strong></a>.</p><p>The reasons are numerous and <a href="https://typescript-is-like-csharp.chrlschn.dev/pages/intro-and-motivation.html">I’ve written about this extensively before</a>, but it’s worth summarizing this again in mid 2026 framed in the context of building in an agent-first reality:</p><ul><li>Build- <em>and</em> run-time type checks provide a foundational layer of safety and correctness to check agent outputs</li><li>Clean, easy to understand build-time error traces; compare this to typical TypeScript type error stacks…</li><li>Roslyn static analyzers that allow for custom static analysis for agent guardrails</li><li>Roslyn source generators for terse runtime code to remove boilerplate and reduce context for agents on the read path</li><li>Extensively and richly documented with broad first-party libraries means that LLMs have very good coverage in their training data. While Python and JavaScript may have <em>more representation</em>, the lack of a standard library and BCL means that there are many variants of how to accomplish the same task.</li><li>EF Core (mature, powerful .NET ORM) provides build-time checks for database schemas and decreases the likelihood of runtime errors from schema changes.</li><li>Mature, stable, <em>consistent</em>, easy to use tooling and tool chain. Just dotnet. Again, easy for agents to work with, minimizing the need for skills or explanatory text for the agents.</li></ul><p>Aside from that, if developers are no longer writing code, why <em>not</em> choose a platform that has:</p><ul><li>Built-in primitives for in-process asynchronous coordination (System.Threading.Channels, Orleans actor model)</li><li>Higher performance and throughput where it matters (boundary serialization for both JSON and <a href="https://github.com/LesnyRumcajs/grpc_bench/discussions/559"><strong>gRPC/Protobuf</strong></a>, etc.)</li><li>Better story around ecosystem security and dependency management (NPM vs Nuget)</li><li>A whole professional organization continuously patching and fixing the platform and libraries</li></ul><p>Given the benefits of adding both build and runtime type safety as well as a mature, well-documented, and broad first party platform, C# is a surprisingly strong choice for building real software with AI.</p><p>The objective of this two-part series is to shed some light on how C# and the .NET ecosystem is an ideal foundation for building production software with agents.</p><ul><li><strong>Part 1</strong> (you are here) will introduce two under-the-radar components of the .NET stack that make it surprisingly amenable to building software with agents: Aspire and CSharpRepl.</li><li><strong>Part 2</strong> will dive into a hands-on implementation from the ground up with an open-source template for teams to build on top of.</li><li><strong>Part 3</strong> will implement the next layer of the application including a simple streaming interface to the Copilot SDK agent.</li><li><strong>Part 4</strong> will extend the application with Testcontainers to demonstrate how to simplify test execution for agents with stateless containers as well as telemetry for runtime visibility.</li></ul><p><strong><em>The full repo</em></strong>: <a href="https://github.com/zeeq-ai/zeeq-tmpl">https://github.com/zeeq-ai/zeeq-tmpl</a></p><h3>Encapsulating the runtime with Aspire</h3><p>If you’re using Docker Compose or <a href="https://tilt.dev/"><strong>Tilt</strong></a> for dev runtime orchestration, you may have occasionally wished that it was just a bit more <em>programmable</em>. That’s exactly the gap that <a href="https://aspire.dev/"><strong>Aspire</strong></a> fills: a programmable orchestration layer that makes it easy to build an isolated runtime stack that agents can control while building software.</p><p>The best analogy is to consider the difference between Terraform and Pulumi or CDK. Terraform is a declarative tool for building infrastructure, while Pulumi and CDK are <em>programmable</em> tools that allows teams to build infrastructure with code. Aspire is the same thing for orchestrating your runtime stack.</p><blockquote><em>Early iterations of Aspire were focused more on building distributed, microservices systems. While Aspire is still great for that, it is even better as an agent-friendly runtime orchestration layer.</em></blockquote><p>It has several key features that make it a foundational component of an agent-friendly software stack:</p><ul><li>It has a <a href="https://aspire.dev/fundamentals/networking-overview/"><strong>built-in internal network loop</strong></a> that isolates individual runtime stacks. This is important for agent development and worktrees because this allows agents to run the full stack in isolation.</li><li>It has a <a href="https://aspire.dev/fundamentals/telemetry/"><strong>built-in, queryable OpenTelemetry target</strong></a> that gives agents the richness to access not just logs, but logical flows through traces and spans with rich metadata attributes on logs, traces/spans, and metrics.</li><li>The aspire CLI supports searching and filtering console logs as well as OTEL structured logs and traces by resource.</li><li>It is deeply programmable and supports C#, TypeScript, and Python for building runtime orchestration logic. And of course, it can run any code and has <a href="https://aspire.dev/integrations/gallery/?"><strong>built-in extensions for handling most common runtime stack components</strong></a> like Vite powered JS frontends, databases, messaging, caching, etc.</li></ul><h3>Built in telemetry and observability</h3><p>A key piece of the story is Aspire’s easy-to-use and built-in telemetry and observability backend.</p><p>This screenshot of the Zeeq Aspire dashboard shows what a typical local runtime looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XX2aI-0YUX4rCmPTUQ0gsA.png" /></figure><p>It doesn’t look too different from Tilt or Docker Desktop, but peek the telemetry:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZpzrpD0NR0Q7BLCrNCV6vw.png" /><figcaption><em>Rich built in telemetry lets agents get visibility into everyday types of issues like the underlying database queries being issued.</em></figcaption></figure><p>Most importantly, the telemetry sink also surfaces gen.ai attributes that give visibility into standard agent interactions: materialized prompts, tool calls, etc.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4WapUc-I8sk_aKgJmnEA_A.png" /><figcaption><em>Aspire materializes </em><em>gen.ai spans to make it easier to see final prompts, tool calls, and agent interactions. The agents can search these spans directly.</em></figcaption></figure><p>You will certainly get much, much richer telemetry via specialized tools like Langfuse (not mutually exclusive since it becomes just another OTEL sink), but Aspire’s built-in, searchable telemetry sink let’s agents autonomously iterate with visibility into the runtime state.</p><pre>aspire otel spans zeeq-server --search &quot;BEGIN CHANGES&quot; \<br>  --non-interactive -nologo \<br>  --dashboard-url http://localhost:15010</pre><p>Searches through the list of spans that the agent can then then read for a full trace of the execution flow (assuming your code has been well-instrumented!):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nCFDOo9B0xTvIJM7yWwVsg.png" /><figcaption><em>The CLI allows agents to search through the telemetry spans before doing a full read to trace through an instrumented execution flow</em></figcaption></figure><h3>Agent interaction with runtime resources</h3><p>The aspire CLI becomes a primary interaction point with agents for runtime resources including:</p><ul><li>Reading logs via aspire logs</li><li>Query the state of running resources via aspire describe &lt;resource&gt;</li><li>Controlling resource lifecycle via aspire resource &lt;resource&gt; start|stop|rebuild</li></ul><pre># Search logs for a given resource<br>aspire logs zeeq-server --search &quot;principal&quot; \<br>  --non-interactive --nologo<br><br># Allow agent to further filter via jq<br>aspire logs zeeq-server --search &quot;principal&quot; \<br>  --non-interactive --nologo --format json<br><br># Check application status with all environment variables<br>aspire describe zeeq-server --format json<br><br># Check application status and query for specific properties<br>aspire describe zeeq-server --apphost ./host --format json | \<br>  jq -c &#39;.resources[] | {<br>    name,<br>    displayName,<br>    state,<br>    healthStatus,<br>    commands: (.commands // {} | keys)<br>  }&#39;<br><br># Rebuild and restart a compiled resource like a .NET or Rust backend<br>aspire resource zeeq-server rebuild<br><br># Restart a Node frontend resource<br>aspire resource zeeq-web restart</pre><h3>Port isolation for worktrees</h3><p>The inner network loop becomes a key feature if you’re using worktrees and you want to <a href="https://devblogs.microsoft.com/aspire/aspire-isolated-mode-parallel-development/">run the full application stack in isolation to allow for agents to work in parallel</a>.</p><pre># Run with randomized ports and isolated user secrets<br>aspire run --isolated</pre><h3>Wiring CSharpRepl into the running application</h3><p><a href="https://fuqua.io/CSharpRepl/"><strong>CSharpRepl (CSR)</strong></a> is a command line REPL that allows interactive C# scripting.</p><p>In the past, this has been generally useful for quickly trying out C# code without having to create a project or compile and run. However, the real unlock with CSR is that it can be <em>wired into a running instance of the application</em>.</p><h3>Direct access to runtime dependencies</h3><p>Imagine the following dependency graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/836/1*HNxvWZXNwZ_XsnmdPUKnzA.png" /><figcaption><em>A typical dependency graph for a web API handler endpoint.</em></figcaption></figure><p>If the agent can only test the web API handler entrypoint via curl, then it cannot easily diagnose issues with Service α since it must always pass through three layers of dependencies before it can test an input scenario with Service α. While some of this can be covered with unit and integration tests, in some cases — especially with AI-enabled apps — there’s no substitute for being able to test the runtime application directly like a human might with a debugger.</p><p>This is precisely what CSR allows. Will Fuqua (author of CSharpRepl)<strong> </strong><a href="https://fuqua.io/blog/2026/06/injecting-a-csharp-repl-into-a-running-net-process/"><strong>has a writeup that shows the gist of this</strong></a>:</p><pre># Get the installation location of the CSharpRepl tool hooks<br>csharprepl connect init<br><br># This yields two environment variables (example) that can be wired up via Aspire<br>export DOTNET_STARTUP_HOOKS=&quot;.../tools/csharprepl/.../connector/CSharpRepl.InjectedHook.dll&quot;<br>export ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=&quot;CSharpRepl.InjectedHook&quot;<br><br># Then list the candidate processes for CSR to connect to<br>csharprepl connect list<br><br># And connect to the PID<br>csharprepl connect 6580</pre><p>Once connected, the agent can then <em>directly manipulate </em><em>Service α</em> and any other runtime dependencies without having to go through the API entrypoint. This allows agents to:</p><ul><li>Diagnose issues and iterate on fixes much more quickly since it can wrap the running functions,</li><li>Test different input scenarios,</li><li>Replace the current implementation and run a live test reaching directly into the DI container,</li><li>Bypass the authentication and authorization layers at the boundary to simulate different users without having complicated agent authorization paths or weakened surface areas to create an affordance to let agents access the runtime.</li></ul><p>Think of it like a blood test versus directly examining an organ or tumor. The blood test is still useful, but being able to directly probe the organ or tumor will give a more complete picture of underlying “operational runtime behavior” and make it easier for diagnosing issues.</p><h3>Wrapping and replacing functions at runtime</h3><p>But it goes beyond that:</p><ul><li>#replace allows agents to intercept and <em>replace</em> a running function with another implementation</li><li>#wrap allows agents to intercept and <em>wrap</em> a running function with additional logic (like logging, telemetry, etc.)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*AoGd6r0UmbkWcA5WQIu9gA.png" /><figcaption><em>How wrap and replace can be used by agents to diagnose runtime issues, insert additional telemetry, and test new implementations rapidly.</em></figcaption></figure><p>Together, this toolset gives agents a <strong>huge</strong> unlock because the agents can reach deep into the runtime configuration of the application to test different scenarios. Three aspects that I find this boosts:</p><ol><li>Agents can better diagnose production issues by directly interacting with a local running instance and simulate the failure conditions or attempt to recreate the failure conditions from a production trace.</li><li>Agents can build more stable runtime systems and regression test backends without the need to use slower frontend browser/client manipulation.</li><li>Agents can experiment and iterate faster while building more stable runtime/production code.</li></ol><h3>Closing thoughts</h3><p>While it is true that a lot of the innovation in the AI space originates from the Python ecosystem, for most teams <em>building business applications using AI</em>, the C# and .NET stack is a highly underrated and perhaps surprising option, especially for folks that haven’t dabbled in C# or .NET since the <em>.NET Framework</em> era.</p><p>Modern .NET and C# are well-suited for teams that want to build real products and real software on a stable base in a programming language and stack that offers agents strong build-time guardrails as well as throughput and performance at scale where it matters (consider the API boundary serialization of JSON and gRPC/Protobuf, for example — both areas where C# and .NET excel).</p><p>In Part 2, we’ll build a practical template for a C# + .NET agent-enabled application that you can use as a starting point for your own projects (wiring EF core for build time checked database queries, minimal web API endpoints, test containers for integration testing, etc.). We’ll also go into real-world examples of how to let agents operate the stack effectively when building autonomously including a skill that guides agents on getting the most out of CSharpRepl.</p><blockquote><em>If you are curious to see a real-world setup, check out the </em><a href="https://zeeq.ai/"><strong><em>Zeeq.ai</em></strong></a><em> repo: </em><a href="https://github.com/zeeq-ai/zeeq-app"><strong><em>https://github.com/zeeq-ai/zeeq-app</em></strong></a></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5a45f46a1ff1" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/the-unexpected-ai-stack-c-net-part-1-5a45f46a1ff1">The Unexpected AI Stack: C# + .NET (Part 1)</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MCP is Dead; Long Live MCP!]]></title>
            <link>https://itnext.io/mcp-is-dead-long-live-mcp-a67bd74a6576?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/a67bd74a6576</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[software-engineering]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Sat, 14 Mar 2026 20:04:56 GMT</pubDate>
            <atom:updated>2026-03-15T18:08:04.805Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cvcSUicayULkG6ZZ.png" /></figure><h3>Summary</h3><ul><li>There is currently a social media and industry zeitgeist dialed-in on CLIs…just as there was a moment for MCP but just a few short months ago</li><li>While it is true that there are token savings to be had by using a CLI, many folks have not considered how agents using custom CLIs run into the same context problem as MCP, except now without structure and many other sacrifices</li><li>In much of the discourse, there is a lack of distinction between local MCP over stdio versus server MCP over HTTP; <em>the latter is a very different use case</em></li><li>Many folks are also only familiar with MCP tools, but overlook <em>MCP prompts and resources</em> as an important org- and enterprise-level mechanism for moving from <em>cowboy vibe-coding</em> to organizationally aligned <em>agentic engineering</em>.</li><li>The importance of MCP auth is also commonly misunderstood as is the role and importance of telemetry in understanding org-wide tool use</li><li>The oversight made by many is that <em>individual usage</em> of coding agents looks very different from <em>organizational adoption</em> of coding agents where there is an emphasis on visibility, telemetry, security, quality, and being able to operationalize and maintain agent-coded systems by a team of varying degrees of skill and experience.</li><li>For enterprise and org-level use cases, MCP is the present and future and teams need to be able to cut through the hype of the moment.</li></ul><h3>The Influencer Driven Hype Cycle</h3><p>Just 6 months back, Model Context Protocol (MCP) was all that anyone could talk about and it seemed that everyone was in a frenzy to ship MCP-related offerings and tools. At <a href="https://www.usemotion.com/">Motion</a>, where I work, we were in the thick of it with seemingly every single vendor trying to get us to buy or use their MCP product.</p><blockquote><em>“It’s just an API; why do I need a wrapper around that when I can just call the API directly.”</em></blockquote><p>This was my common response to these vendors as they tried to pitch usage of their MCP offerings (often at a premium!). My guard was up as I looked on from the sidelines at the hype surrounding MCP with skepticism; we entirely skipped the MCP hype cycle because it wasn’t clear what the added value was.</p><p>But in just 6 short months, it seems as if the industry discourse around MCP has completely shifted. <em>What happened?</em></p><p>First, most folks realized that in most use cases, MCP is just overhead to calling APIs directly because MCP as a wrapper for APIs do not make sense. Instead of using MCP, we simply wrote small tool wrappers around REST API endpoints.</p><p>Second, it is instructive to step back and understand just how much of this field is now driven by individuals marketing their companies and marketing themselves. So much of the AI landscape requires creating a sense of FOMO and hype and has been that way for years (while those of us adopting and adapting these tools on a daily basis often look on scratching our heads). These influencers constantly need <em>some</em> content to rant about to stay relevant and push themselves, their products, their companies, or their services so we see constant re-orientation around the zeitgeist of the moment.</p><p>In that sense, I view many of these influencers (and even people that should know better like Garry Tan and Andrew Ng) as no different than influencers peddling Ivermectin as a cure-all or anti-vax conspiracies; it’s simple ignorance.</p><p>The influencer-driven discourse has now shifted towards hating on MCP and praising CLIs as the <em>current</em> zeitgeist. Not without reason (to be clear) because in many cases, it is true that the CLI makes much more sense as an interface for agentic tool use…except when it does not.</p><h3>Understanding the Misunderstanding</h3><p>Before we dive any deeper, I would like to make clear that I think <strong><em>MCP is indeed the wrong choice for a class of use cases</em></strong>, but as the title might suggest, there is a deep misunderstanding of where and how CLI tools can yield savings and the important role of Model Context Protocol in moving orgs and enterprises towards more structured engineering with AI coding agents.</p><p>There are two topics at the core of the debate of CLI vs MCP and the foundation of much of the discussion:</p><ol><li>Token savings and efficiency with CLIs.</li><li>The bloat and uselessness of MCP and its complexity.</li></ol><h3>Do Token Savings Exist?</h3><p>Yes, they do. There are three modalities in token savings to be had, but it might not be as dramatic as social media would have you believe.</p><h4>CLI Tools in the Training Dataset</h4><p>First is that CLI utilities <em>in the models’ training dataset</em> like jq, curl, git, grep, psql, aws, s3, gcloud, etc. benefit tremendously from the underlying models already having encountered <em>innumerable examples</em> of how to use these tools. Because of this, the agent does not need additional instruction, schemas (not true when calling custom REST APIs, though), or context on how to use these tools; it can simply one-shot the tool in many cases. This can be a significant savings over MCP because in MCP, the tools must be declared up front in the tools/list response.</p><p>For CLI tools that will already be in the agent’s training dataset, <strong><em>absolutely</em></strong> always prefer them over MCP (for custom REST APIs, that’s a “maybe”).</p><p>However, this is <em>not</em> true of a custom, bespoke CLI tool. The LLM has no way of knowing which CLI to use and how it should use it…unless each tool is listed with a description <em>somewhere</em> either in AGENTS|CLAUDE.md or a README.md. You <em>must</em> provide the LLM some instruction on how and when to use a bespoke CLI tool that it has never seen before.</p><p>It is possible to point it to a directory called /cli-tools and rely on descriptive naming to have the agent pick and choose the tool, but anyone that works with agents day in and day out already knows that agents are often not very good at this without more explicit instructions. It will make mistakes and you will have to update your AGENTS|CLAUDE.md or other docs somewhere and fill it with more and more descriptions each time you find the agent misbehaving with your bespoke CLI tools.</p><p>Aside from that, even with a tool like curl, any token savings are lost the moment the agent has to understand a bespoke OpenAPI schema to correctly call the API since the entire OpenAPI schema may need to be loaded into context or extensive examples given to the agent in context to instruct it on how to use it. <em>Oops</em>; there goes all of your hyped up context savings.</p><h4>Chained Extraction and Transform</h4><p>In some workflows, it is possible to use a chain of CLIs to perform retrieval and then a transform or use extraction to reduce the data that makes it into the context window. In this scenario, there can be significant savings to be had, but it is also not unique to CLIs since most content that follows a format (HTML, JSON, XML, etc.) can be extracted using standard libraries via selectors (DOM/CSS, JSONPath, XPath, etc.). I don’t see this as a unique capability of a CLI based approach, but rather an underutilized approach because it is generally easier to just stuff it into context and let the LLM figure it out and optimize later.</p><h4>Progressive Context Consumption</h4><p>It is true that CLIs allow for progressive context consumption as an agent tries to deduce usage. Whereas MCPs will state the toolset and schema up front (the “bloat”), CLI tools can progressively load their --help into context.</p><pre>// Sample from the Model Context Protocol site<br>{<br>  name: &quot;searchFlights&quot;,<br>  description: &quot;Search for available flights&quot;,<br>  inputSchema: {<br>    type: &quot;object&quot;,<br>    properties: {<br>      origin: { type: &quot;string&quot;, description: &quot;Departure city&quot; },<br>      destination: { type: &quot;string&quot;, description: &quot;Arrival city&quot; },<br>      date: { type: &quot;string&quot;, format: &quot;date&quot;, description: &quot;Travel date&quot; }<br>    },<br>    required: [&quot;origin&quot;, &quot;destination&quot;, &quot;date&quot;]<br>  }<br>}</pre><p>But nonetheless, the reality is that unless it is a well known tool in the LLM’s training dataset, the agent will need to progressively (and over multiple turns) descend the CLI tools’ help content to understand the available commands, sub-commands, and parameters.</p><p>There is simply no getting around this because the agent has no way of knowing how to use a bespoke CLI tool otherwise.</p><pre># What it might look like as --help output<br><br>command: searchFlights   Search for available flights<br>                         input: JSON object with origin, destination, date<br>                         example:<br>                         {<br>                           origin: &quot;(string; required) departure city&quot;,<br>                           destination: &quot;(string; required) arrival city&quot;,<br>                           date: &quot;(date:yyyy-MM-dd; required) travel date&quot;<br>                         }</pre><p>I don’t know about you, but this sure looks like the MCP schema…just without any structure.</p><p>It is true that this could be progressively loaded instead by first listing all of the commands and then having the agent only --help the desired command to disclose the costly payload descriptor for the tool:</p><pre># Progressively loading instead of loading the entire schema<br><br>For usage:<br><br>flights &lt;command&gt; [--help]<br><br>commands: searchFlights      Search for available flights<br>          bookFlight         Book a flight<br>          ...</pre><p>However, I would make five points here:</p><ol><li>For a sufficiently complex flow, the agent will end up traversing most of the tree, regardless.</li><li>The likely context savings will end up pretty minimal if the MCP toolset is smartly designed in the first place; the agent just ends up taking more turns with a CLI while progressively discovering commands and parameter descriptors.</li><li>Without giving the agent the full schema up front, the chance of the agent using the toolset correctly will go down. In the same way that <a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals">Vercel found agent usage of docs improved when they placed the full doc index into </a><a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals">AGENTS.md</a>, our intuition should tell us that if the agent is aware of all of the tools and parameters at the outset, it will be better equipped to select the right one.</li><li>Don’t give your agent complex, useless MCP tools in the first place? CLI and MCP are not mutually exclusive. Be selective in both cases.</li><li>With Anthropic also offering 1m context window now, is this still the defining argument to make?</li></ol><p>If you’re still not convinced that a lot of this discourse lacks nuance and is just hype, <em>congrats on buying into the current AI-influencer FOMO hype cycle</em>; see you in 6 months when the influencers move on to the next revelation of the moment to stay relevant and get your eyeballs and dollars.</p><h3>The Duality of MCP</h3><p>When the MCP hype first started, I was as much a skeptic as anyone. Motion, at the time, was building out our “AI Employees” (like every other hype-chasing team) and building integrations.</p><p>While vendors were trying to sell us on their MCP implementations of integrations, we simply wrote tool wrappers around REST API calls and passed API keys or auth info in headers using standard Bearer tokens. The hype seemed entirely unjustified and I looked on with skepticism that MCP had any future whatsoever.</p><p>Especially in stdio mode, MCP felt excessive and useless. Indeed, in most use cases, MCP over stdio is probably not needed and adds complexity over writing a simple CLI.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/876/1*iw38uUtFVzvfepeC6GLpyw.png" /><figcaption>It is important to make a distinction between the two different models of accessing MCP locally over stdio and remotely over streamable HTTP.</figcaption></figure><p>But MCP over streamable HTTP? <strong><em>This is an absolute game changer</em></strong> and will be a key linchpin in organizational and enterprise adoption shifting from <em>vibe-coding</em> to <em>agentic engineering</em>.</p><h3>Why Centralization is Key</h3><p><em>(I preface that this is primarily relevant for orgs and enterprises; it really has no relevance for individual vibe-coders)</em></p><p>Most influencers talking about MCP fail to make a distinction between how MCP communicates locally over stdio mode versus streamable HTTP.</p><p>In stdio mode, the MCP server runs locally with the agent and indeed, why bother with this over writing a simple CLI?</p><p>But when accessed over streamable HTTP transport, it is possible to run that same logic <em>in a centralized server</em> and there are several unlocks when deployed and accessed by the agents under this modality.</p><h3>Richer Underlying Capabilities</h3><p>What if the use case would benefit from having access to a Postgres instance? With <a href="https://github.com/apache/age">Apache AGE</a> enabled for Cypher graph queries over indexed context? This is very straight forward when the tooling is centralized on a server and accessed by a thin client. It is possible to implement richer platform capabilities for the tooling because distribution is as simple as pointing an agent to an HTTP endpoint and adding an auth token.</p><p>Yes, a local database like SQLite may also do the trick, but there is a limit to what’s possible and sharing state across an org becomes more difficult.</p><h3>Ephemeral Agent Runtimes</h3><p>Remote MCP servers over HTTP also offer big advantages depending on the runtime context.</p><p>For example, when exposing remote tools and APIs to agents running in GitHub. Here, MCP makes it trivially easy to use tools that could require complex backends without any install and without restrictions of operating in the ephemeral nature of the GitHub Actions runtime environment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/717/0*eHiFf8kFUQXmRfUq.png" /><figcaption>GitHub Agents configuration for MCP allows pulling in remote tools to the GitHub Actions runtime.</figcaption></figure><p>It offloads management of stateful workloads in stateless, ephemeral contexts to a centralized server.</p><h3>Auth and Security</h3><p>Moving workloads into a server also improves the story around auth and security. CLIs that need to access secured API endpoints using curl, as an example, require that every developer have access to keys to those APIs (or proxy calls through some server). It’s easy to see why this is bad and a pain in the ass for an ops teams.</p><p>Centralizing this behind MCP allows each developer to authenticate via OAuth to the MCP server and sensitive API keys and secrets can be controlled behind the server like any other bog-standard server REST API using (more or less) bog-standard OAuth. The exposure of secrets is controlled, limited, and easy to audit.</p><p>An engineer leaves your team? Revoke their OAuth token and access to the MCP server; they never had access to other keys and secrets to start with.</p><h3>Telemetry and Observability</h3><p>For teams, another big win with centralized streamable MCP is the story around telemetry and observability. Which tools are having an impact? Which agent runtimes are the team using? Which tools are low value? Where and how are tools failing? Without centralized, standardized telemetry, it is exceedingly difficult as an engineering org to understand what’s working and what’s not.</p><p>With a centralized MCP server, this is simply a matter of emitting OpenTelemetry traces and metrics and collecting them using standard, off-the-shelf tooling.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/900/0*586W0FMSdVvK9i62.png" /><figcaption>A DataDog dashboard we use for internal tracking of tool activation and content access. This allows us to see which content has high impact in the org and where we should invest in docs and tooling as well as visibility into the tooling used by our team.</figcaption></figure><p>While this is achievable with CLI tools, it is far easier to do so when deploying a single server <em>because local delivery requires consumers to update</em> and a lot of the scaffolding that happens centrally now has to be reproduced on each CLI tool that is shipped by a team.</p><h3>Standardized, Instant Delivery of Up-To-Date Content</h3><p>Distributed tooling (e.g. via packages) has much the same problem as working with distributed apps: keeping deployments up to date with the latest release and API compatibility. While it is relatively straightforward to update a server to add new capabilities, if tool capabilities are delivered as local CLI tools that interact with those APIs, now API version compatibility becomes a concern.</p><p>MCP includes provisions for this: <a href="https://modelcontextprotocol.io/specification/2025-11-25/architecture#clients"><strong>subscriptions and notifications</strong></a> which allow servers to call back to the client notifying them of updates.</p><p>While most folks are aware of <em>MCP tools</em>, relatively fewer are aware of <em>MCP prompts</em> and <em>MCP resources</em>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/886/0*Z7fyTnBJG5RDnCWy.png" /><figcaption>From the MCP protocol docs web site. Note the mapping of MCP prompts to skills, MCP resources to docs.</figcaption></figure><p>Look carefully and you can see:</p><ul><li>MCP Prompts are effectively <em>server-delivered</em> SKILL.md</li><li>MCP Resources are effectively <em>server-delivered</em> /docs</li></ul><p>Why would an org, team, or enterprise want this?</p><p>Well, it’s easy to see several benefits.</p><ol><li><strong>Dynamic content</strong>. While *.md files in a repo are static text files that need to be manually updated, synced, and maintained, server produced prompts and resources have no such restrictions. It is possible to dynamically generate the text on the fly to construct a skill. What about docs? What if some docs are useful in some contexts, but not others? What if docs could benefit from dynamic injection of content/context without a tool call (pricing data, current system status, etc.)?</li><li><strong>Automatic and consistent updates</strong>. When *.md files are delivered as part of the repo or as part of a package import, the downside is that they can become out of sync and require explicit syncs on the local system. This is not the case with server-delivered skills and resources via MCP prompts as these are always up to date. What about third party, official sources of docs and skills? Do you manually reproduce and update these in your repo? Or would it be easier to simply proxy it through a server?</li><li><strong>Org-wide knowledge</strong>. Some content applies org-wide. For example, standard best practices for security or telemetry in apps or infrastructure deployment considerations for apps. What about orgs that use microservices where one team may need to have docs for another service. Or what if a service team could provide skills for their service dynamically each time they ship? Does it make sense to reproduce these docs into every repo? How does an org keep them up-to-date and in sync?</li></ol><p>MCP is the answer to all of these.</p><p><strong><em>If you want to deliver standard planning prompts (skills) and standard docs across all repositories, all workflows, all teams, and all agent front-ends (Claude Code, Codex, VS Code Copilot, GitHub Agents, OpenCode, etc.) consistently and always up-to-date with server telemetry to measure adherence and usage across an org, then MCP prompts and resources IS that delivery mechanism.</em></strong></p><p>Here is an MCP delivered prompt in OpenCode:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/958/0*XzuGxyPrjyjguRWQ.gif" /><figcaption>MCP prompts exposed in OpenCode</figcaption></figure><p>And a set of MCP delivered resources:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/958/0*FPXeV7AnD7-K44Nq.gif" /><figcaption>MCP resources exposed in OpenCode. Here, each resource is a “virtual” index generated from a partition of documents.</figcaption></figure><p>The same in Claude Code CLI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/958/0*yaCCib3cB7qJasuy.gif" /><figcaption>Claude Code CLI showing the same capabilities.</figcaption></figure><p>Each of these resources is a dynamically generated, “virtual” index of documents that are available (similar to Vercel’s index in AGENTS.md) and we have the benefit of bringing these documents into any project, always up-to-date, and with full server telemetry on which documents are being accessed.</p><p>In all cases, delivery of this capability requires only configuring the MCP client; it is fire and forget with no need to keep things updated on the client.</p><h3>Closing Thoughts</h3><p>I get it; the industry moves fast now and these social media influencers need to keep chasing something new to keep their audience engaged. 6 months ago, MCP was the hot ticket. Now it’s a has-been; blamed for context bloat while often not even considering the tradeoffs and similar traps with custom CLIs. The masses following along have seemingly lost all critical thought in so far as engineering discipline goes.</p><p>But a simple thought exercise of how teams can move engineering orgs from vibe-coding towards agentic engineering would land one pretty squarely on the design and mission of the Model Context Protocol. In any scenario beyond a solo vibe-coder, MCP’s telemetry, simplified considerations for managing security, automatic content synchronization, schema + standards based approach, and ease of observability (how can you tell which tools are effective otherwise?) mean that teams that buy into the current zeitgeist will make a mistake when selecting an approach for delivering the scaffolding that enables agentic engineering.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RVHn-MJn0_2QOfFO.png" /><figcaption>Amazon’s recent challenges with AI assisted coding affecting real, production systems</figcaption></figure><p>We are still relatively in the early days of AI agents taking a leading role in software engineering, and because the field moves quickly, there is an emphasis on speed at all costs. But as we’ve seen with <a href="https://arstechnica.com/ai/2026/03/after-outages-amazon-to-make-senior-engineers-sign-off-on-ai-assisted-changes/">Amazon’s recent challenges in their AWS division</a>, <em>teams eventually have to operationalize and maintain these software systems produced by AI agents</em>.</p><p>Take advice and anecdotes on agents from the likes of Garry Tan and Andrew Ng with a grain of salt: what works for them <em>*individually*</em> in a homogeneous environment likely will not hold true for teams and orgs in a heterogeneous environment where the objective is to ensure a team of diverse skill and experience levels converges upwards in their output and production of software.</p><p>If the goal is to ship software that is dependable and maintainable once operationalized, we still need an engineering discipline that ensures consistency, security, high quality, and correctness — even when the producer of that software is an AI agent. Organizations need architectures and processes that start to move beyond cowboy, vibe-coding culture to organizationally aligned <em>agentic engineering</em> practices. And for that, MCP is currently the right tool for orgs and enterprises.</p><p>Long live MCP!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a67bd74a6576" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/mcp-is-dead-long-live-mcp-a67bd74a6576">MCP is Dead; Long Live MCP!</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How Mindset Shapes Engineering Success at Startups]]></title>
            <link>https://chrlschn.medium.com/how-mindset-shapes-engineering-success-at-startups-4e231ebfd5db?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/4e231ebfd5db</guid>
            <category><![CDATA[psychology]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[startup]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Sun, 14 Dec 2025 21:50:44 GMT</pubDate>
            <atom:updated>2025-12-14T21:50:44.557Z</atom:updated>
            <content:encoded><![CDATA[<p>Thinking about joining a fast-paced, high-growth startup? Trying to hire the right team to accelerate your growth? Aiming to build a high-performing, low-ego engineering team? Then there’s probably one key character trait to focus on — <em>and it has nothing to do with engineering talent</em>.</p><p>Just over a decade ago, Satya Nadella took over the helm at Microsoft from Steve Ballmer and instrumented perhaps one of the biggest recent turnarounds in Big Tech.</p><p>Prior to Nadella’s turn at the helm, Microsoft’s business model was still tightly focused on Windows licensing with cloud seemingly an afterthought. Over the last decade, Microsoft’s transformation has been nothing short of spectacular as Nadella drove a rapid pivot from the desktop era to the cloud computing era while playing catch-up with AWS. Had Nadella not righted the ship at that moment in time, it’s safe to say that Microsoft could have gone the way of IBM before them.</p><p>At first blush, it may seem odd to correlate Microsoft’s trajectory with startups; after all, what does a multi-trillion dollar behemoth like Microsoft have in common with startups?</p><p>It turns out that many startups can probably learn from Nadella’s embrace of <em>growth mindset</em>. <a href="https://www.inc.com/marcel-schwantes/satya-nadella-says-3-words-define-successful-people.html">In an interview with Inc.</a>, Nadella shared that successful people are defined by 3 words: <strong>Learn-It-All</strong></p><blockquote>[Say] you have two students — one of them has more innate capability, and the other has less. The person who has less, but is a learn-it-all, will ultimately [become] better. That applies to CEOs, and that applies to companies. <strong>I think it has been a helpful cultural metaphor for us to say that you can’t act like a know-it-all; you have to be a learn-it-all.</strong></blockquote><p>This he had learned through the work of a Stanford psychologist: Carol Dweck.</p><h3>Growth Mindset</h3><p>Dweck’s book <em>Mindset: The New Psychology of Success</em> introduced this term and defined the difference between a “<em>growth mindset</em>” and a “<em>fixed mindset</em>”.</p><p>Through her research, Dweck differentiates individuals who view traits like ability and intelligence as innate and immutable (fixed mindset) and those who view these traits as flexible (growth mindset). While it may seem a simple and trivial distinction, in fact, it is probably one of the key discriminators of those who will thrive at startups and those who will wither.</p><p>Listen to enough episodes of the <em>How I Built This</em> podcast, and it is clear that successful startups and founders are those that embraced the growth mindset. Intuitively, this makes sense: just about every successful startup zig-zags their way through a maze of failures, rejections, pivots, and crises. Those who approach this with a fixed mindset will capitulate before finding traction.</p><h3>What This Means for Engineers</h3><p>One of the challenges for engineers when joining a startup is that it can often be quite chaotic and overwhelming. Unlike Big Tech where there tends to be more well-defined processes, isolated teams, stable tooling and platforms, and carefully planned on-boarding flows, startups more often than not require individuals to jump right into the fire.</p><p>For many engineers (especially more senior ones), this can often lead to fast burnout and feelings despair with a heavy onset of imposter syndrome that can lead down an irrecoverable path of self-defeat. One hypothesis why this might have a stronger effect on more senior engineers is that they might have moved on from their growth mindset phase. This is why individually having a growth mindset (<em>and organizationally embodying a growth mindset</em>) is key: because it helps otherwise talented engineers find their footing.</p><p>When <a href="https://medium.com/u/60be22907ef3">Lefan Tan Jia Hui</a> reached out after joining Motion, he sought guidance on how to be productive and how to cope with the incredibly complex, multi-million line codebase at Motion. He had already been a high performing engineer (despite his short resume) that had tackled multiple complex projects across a variety of stacks (Next.js, React, Vue, Astro.js, C#, Postgres — he tackled them all with aplomb!). But at Motion, he found himself struggling to gain his footing; he started to question his own ability and competency.</p><p>To help right his mindset, I sent him the following LinkedIn post and left him with some simple advice: <strong>“embrace looking like an idiot.”</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/810/1*Xkly3WQg3KJ0yLaI4f6L-A.jpeg" /><figcaption>“Embrace looking like an idiot” is perhaps one of the best pieces of advice any engineer can internalize as they prepare to jump into difficult and challenging problems.</figcaption></figure><p>This advice embodies what it means to have a growth mindset and is the key to finding success in almost any undertaking, but holds especially true for startups because there is simply less of a support apparatus in most cases. To get a footing and rapidly contribute requires simply letting go of ego and putting in the work while reflecting on the feedback.</p><p>Scott Brown — a carpenter with a fantastic YouTube channel— <a href="https://www.youtube.com/clip/UgkxIwf3Yhmr4Pb3KlcBWE4DPghq9PPVgSSP">has one of my favorite clips on this</a> that I often share as well when folks ask for advice on how to get better:</p><blockquote>That’s not how you get better. The way you get better is by putting something out and going “Well, I’ll do better on the next one”. And then you do that week after week, month after month. Before you know it, your first video and your most recent video don’t look anything alike.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RP2Aoc0JMvx69APzsNKEQw.png" /><figcaption>Scott Brown reflecting on his 100th video and how he progressed from his first videos. <a href="https://www.youtube.com/clip/UgkxIwf3Yhmr4Pb3KlcBWE4DPghq9PPVgSSP">https://www.youtube.com/clip/UgkxIwf3Yhmr4Pb3KlcBWE4DPghq9PPVgSSP</a></figcaption></figure><p>This perspective is important as engineers can view their <em>current state</em> and their <em>current expertise</em> as “fixed intrinsics” and that their value is in this <em>existing expertise</em> whereas thriving in a startup actually requires engineers to frequently step out of their comfort zone, learn new skills, deal with new complexities, and grow rapidly.</p><h3>What This Means for Startup Teams</h3><p>Many teams fail to foster this kind of environment and end up creating the opposite: an environment driven by the need to protect one’s ego. When this takes hold across the team, <em>it will find itself accidentally oriented around a fixed mindset driven by ego — </em>the exact opposite of what is desirable in a fast-paced, high-growth environment<em>.</em></p><p>Gather a group of smart founders and talented engineers and it can be easy to create an environment where “<em>I don’t know</em>” or “<em>Why</em>? <em>How?</em>” become sacrilegious phrases when, in fact, these are some of the most powerful phrases in a team’s common grammar — so long as the team is oriented around a growth mindset.</p><p><a href="https://motionapp.notion.site/Motion-Company-Culture-21bd766b1f864764b0db2fa72529b182">Motion’s Culture Document </a>explicitly emphasizes this mindset:</p><blockquote>We encounter a lot of hard problems, most of which don’t have obvious answers. We aren’t constrained to any particular solution, and sometimes the best solution may sound crazy at first. We want an environment where every member of the team feels excited sharing new ideas, which will be respected and challenged by others.</blockquote><p>There are many systematic ways that teams can right the ship and foster this culture:</p><ul><li>Leaders have to embody this and be ready to say “I don’t know” and always be openly seeking the expertise of others. This sets the tone that “not knowing” is not the same as “not capable”. “Not knowing” is always the first step on the path to true knowledge.</li><li>Shape the onboarding process so that new hires understand that they are expected to <strong>“embrace looking like an idiot” </strong>for the first few weeks. Candidates may not have the right perspective that they were selected through an expensive and time consuming process and the team is making a bet on them. They may hyper-focus on their <em>current ability</em> rather than their ability to grow as the key reason for their selection. Make it clear that the goal is for them to learn fast and the best way to learn fast is by asking questions and making some mistakes along the way rather than protecting their ego. The more senior a hire, the more important it is to focus on this messaging because for these hires, they likely have their own high expectations.</li><li>Foster a low-ego culture where even the most senior and capable resources (perhaps especially these) openly probe with genuine curiosity rather than probing maliciously with an attitude of self-preservation of the ego.</li><li>Focus on a culture of documentation and sharing of responsibility (rather than singular ownership) as this creates an environment where knowledge is <em>not</em> siloed. Siloed knowledge can become a precursor to an ego-driven culture because it inflates the importance of the individual rather than the prosperity of the team. Documentation and transferability of knowledge are systematic ways to curtail this. Some engineers take the wrong view on this; they see siloing as protecting their job security and self-importance by creating a closed region of the company. Instead, they are simply falling into a fixed mindset of guarding their small empire. Successful leadership will ensure to equate growth with transferability of responsibility; to take on new and impactful work necessarily requires handing off established and stable work.</li></ul><p>Teams and leaders should actively foster a culture of open comms, asking for help, and visible collaboration (especially important for remote teams) as this creates an operating environment where individuals focus on moving the ball forward rather than protecting their ego.</p><h3>Shifting Hiring</h3><p>One key mistake that many startups make is to confuse “badges” for “technical competency”. This is not to disparage the many supremely talented and capable engineers at Big Tech, but a caution that competency at a Big Tech may not transfer to a fast-paced startup environment because of the mindset shift.</p><p><em>(Author’s aside: I call this “badge dropping” and is one of my biggest pet peeves as badges can sometimes be a sign of being a good interviewer rather than being a great engineer.)</em></p><p>In fact, Harj Taggar, a YC partner dedicated a slide to this in one of the Startup School sessions where he discussed his experience coaching hundreds of startup teams:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Gsz2PXlvYVAQB9Xf9io1VA.png" /><figcaption>Harj Taggar cautioning teams from over-indexing on “badges” when hiring founding engineers. Harj advised teams to focus on builders instead.</figcaption></figure><p>Harj’s key point: many FAANG engineers will not thrive in a startup environment not because they do not have the technical ability, but because they have the wrong expectations and <em>mindset</em>.</p><p>Intuitively, there are several reasons why this approach of badge-driven hiring can be a flawed approach for high growth startups:</p><ul><li>In Big Tech, there is a lot of scaffolding already in place for most engineers to take advantage of. Many engineers are thus not broadly skilled in various areas of software delivery (e.g. CI, databases, actual implementation of systems architecture, etc.). In startups, everyone needs to be able to pitch in, even if outside of their technical comfort zone. To do so successfully requires one to oriented their mindset towards growth.</li><li>Big Tech generally has many processes and procedures in place that are shaped for their specific operating environment. Those processes and procedures are more often than not antithetical to how high-growth companies like startups need to operate and individuals can bring that culture with them.</li><li>Career growth in Big Tech often demands siloing — a dangerous trait for startups.</li><li>Big Tech approaches to solving engineering challenges are often too heavy and complex for the needs of fast moving startups where teams should favor flexibility and seek approaches that bias towards adaptiveness.</li></ul><p>Indeed, at one e-commerce startup which received an <strong><em>$8 million seed round</em></strong><em> </em>partially predicated on hiring an all-Amazon engineering team, it quickly became clear within the first two weeks of joining that half of the engineering team simply had the wrong mindset to thrive at a startup. While some individuals embraced the autonomy, speed, and occasional chaos, others were clearly lost in that transition and required either a lot of hand-holding or would end up creating overly complex solution approaches that lacked flexibility and adaptability often required for meandering paths that startups undertake.</p><p><em>The other half?</em> These were the engineers that embraced a growth mindset and quickly adapted while learning new skills, new platforms, and new technologies. Case in point: the team started with cumbersome Elastic Beanstalk based deployments driven by AWS CodeBuild because it was what the team had used in Amazon. But in a highly agile environment, a container-based deployment approach on Elastic Container Service better suited the fast-changing needs of the infrastructure as the product bounced between pivots.</p><p>Many hiring teams perhaps over-index on “badges” and under-index on “growth mindset” (which carries with it some other character traits like low ego and high adaptiveness). For Motion, a key objective is to find the individuals who are not only talented, but also bring with them a low-ego, growth mindset to help rapidly ship products to our customers.</p><h3>Closing Thoughts</h3><p><a href="https://motionapp.notion.site/Motion-Company-Culture-21bd766b1f864764b0db2fa72529b182">Motion’s Culture Document</a> is one of the key reasons I made the choice to join the team as it aligned with my own ideals of an operating culture. Having speed run multiple startups over the last 5 years, one key lesson I take away from those experiences is the importance of “vibes” or culture.</p><p>As Brian Chesky of Airbnb opined in his essay <a href="https://medium.com/@bchesky/dont-fuck-up-the-culture-597cde9ee9d4"><em>Don’t Fuck Up the Culture</em></a><em>,</em></p><blockquote>Why is culture so important to a business? Here is a simple way to frame it. The stronger the culture, the less corporate process a company needs. When the culture is strong, you can trust everyone to do the right thing. People can be independent and autonomous. They can be entrepreneurial.</blockquote><p>For Motion and other startup engineering teams, this can often be one of the biggest hurdles to get right and one of the easiest to stray from. Even a few bad hires or the wrong growth framework can lead to a virulent degradation of culture. Fostering a culture focused around a growth mindset and actively seeking “learn-it-alls” can right that ship.</p><p>For candidates and new hires joining startup teams, simply “<em>embrace looking like an idiot</em>” for the first few weeks and get as much feedback as you can. If an organization doesn’t foster, support, and embody that mindset? Then you’re <strong><em>not</em></strong> going to have a good time and better to cut your losses early!</p><p>If you have a growth mindset and you want to work with a low-ego, high-autonomy team, check out <a href="https://www.usemotion.com/careers"><strong>Motion’s Careers page</strong></a> and hit the apply button! We are growing rapidly; building greenfield projects in C#, TypeScript, React, Postgres; and always exploring creative (<a href="https://grugbrain.dev/#grug-on-complexity">Grug-brained</a>) engineering approaches to solving high-value business problems!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4e231ebfd5db" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Beware the Complexity Merchants]]></title>
            <link>https://chrlschn.medium.com/beware-the-complexity-merchants-eb5ff4128b3d?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb5ff4128b3d</guid>
            <category><![CDATA[software]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-architecture]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Fri, 16 May 2025 03:03:50 GMT</pubDate>
            <atom:updated>2025-05-16T13:04:03.126Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/428/0*kRdYb44GP5G5R0Nk.gif" /><figcaption>From the Wikipedia entry for Rube Goldberg: <a href="https://en.wikipedia.org/wiki/Rube_Goldberg">https://en.wikipedia.org/wiki/Rube_Goldberg</a></figcaption></figure><p>Something I’ve come to appreciate over my career in software engineering is simplicity and how it is an enabler of speed and creation.</p><p><a href="https://www.infoworld.com/article/2270714/complexity-is-killing-software-developers.html">Scott Carey wrote a great essay on this: <em>Complexity is killing software developers</em></a>:</p><blockquote>“Complexity kills,” Lotus Notes creator and Microsoft veteran Ray Ozzie famously wrote in a 2005 internal memo. “It sucks the life out of developers; it makes products difficult to plan, build, and test.”</blockquote><p>We have all felt this friction at some point in our careers where seemingly simple tasks are met with painful papercuts wrought by complexity. (It often reminds me of that scene in the movie <em>Saw</em> where one of the captives has to dive into a pit of needles to find a key.)</p><p>Fred Brooks famously made the distinction between complexity of the “accidental” and “essential” kind thinking, at the time, that many advancements had rooted out much of the accidental complexity in software engineering. Brooks might be quite disappointed at the reality that we face today.</p><p>From that same article, Carey quotes:</p><blockquote>Justin Etheredge, cofounder of the software agency Simple Thread, helpfully differentiates between essential and accidental complexity. He told InfoWorld, “Essential is the complexity in the business domain you are working in, the fact that enterprises are extremely complicated environments, so the problems they are trying to solve are inherently complex. The other area is accidental; this is the complexity that comes with our tooling and what we layer on top when solving a problem.”</blockquote><p>This distinction probably spells out the reason you’ve read this far: because you too, know the frustration of having to deal with accidental complexity while trying to solve the valuable problem of the essential complexity.</p><p>The challenge is that when a group of smart engineers are gathered, it can often be a difficult exercise of discipline to keep them from reaching for complexity of the accidental kind to get their highs.</p><h3>Complexity is the Enemy of Value</h3><p>When foundational systems have been addled by accidental complexity, the net result is that it:</p><ul><li>Unnecessarily creates drag on overall team velocity</li><li>Creates an unstable foundation to build upon</li><li>Focuses the efforts of smart engineers on work that does not correlate to business value</li><li>Creates a culture of gatekeeping by making it difficult if not impossible (or at least highly discouraged) for the broader team to contribute</li></ul><p>These systems are often brittle and difficult to own, understand, and scale because well, <em>that was actually the intent from the start</em>…</p><h3>The Darker Side of Accidental Complexity</h3><p>While accidental complexity can arise innocently enough from well-intentioned engineers, the darker side is that it can manifest from ego and self-preservation. For you see: building a simple, stable, easy-to-use, easy-to-own, and easy-to-operate system means that such a system does not need nearly as much manpower, attention, and of course <em>budget</em> to own. If there are no fires to fight, then there is no need for a firefighter.</p><blockquote>How else shall the complexity merchant demonstrate their value and build their fiefdom within the enterprise but to create the context for which ever more complexity is layered?</blockquote><p>To make things simple from the get-go is not exciting to the complexity merchant and is also not profitable. How else shall the complexity merchant demonstrate their value and build their fiefdom within the enterprise but to create the context for which ever more complexity is layered to solve the previously added complexity? Of course the complexity merchant seeks to command more resources because can’t you see? <em>How else will all of this complexity be solved?</em></p><p>For the complexity merchant to operate, they must always introduce a bit of friction and pain (this self-inflicted injury is how you will know that you are dealing with a complexity merchant). It is the presence of this friction and pain that grants them the justification to peddle their solutions (without it, how else could they expand their influence if everything were easy and smooth?). If you only bring on this tool or hire more engineers or add this vendor, then everything will be solved!</p><p>All along, they will tell you that such complexity is unavoidable and essential; such problems, they will say, have no simple solutions. They will tell tales of Airbnb and Facebook and Google and how yes, you — your team — also <em>needs</em> this complexity. <em>You want to be Facebook, don’t you?</em></p><p>Even in cases where there is no malicious intent, it can be driven by the need to feel accomplished by summiting that peak of complexity.</p><p>These are individuals that have let their ego drive their decisionmaking.</p><h3>A Path to Undoing Complexity</h3><p>As a parent, I often grumble and reprimand my kids if they start playing with something new without cleaning up and putting away what they were playing with previously. It is a simple rule to keep one’s space clean and organized and also imparts a basic sense of responsibility upon the child.</p><p>One simple solution to solving for new complexity? Make the engineers clean up after themselves first. No new complexity until the old complexity has been stabilized or simplified so that anyone can operate and maintain it and keep it in a stable state. No toys left in a state of disarray; all things orderly in their place where they belong at the end of the day. No half-finished promises and half-delivered (of course undocumented) solutions.</p><p>Even better to be skeptical of Silver Bullets and favor tried, true, and boring solutions and approaches for these rob the complexity merchant of their grip. Where complexity is necessary or actually adds business value, it must be contained and there must be a path to transition ownership of that complexity that includes handing it off in a stable, well-documented state.</p><p>Demand documentation and transferability of systems since obfuscation and gatekeeping are key tools of complexity merchants. The more push-back encountered for these basic demands, the more sure you can be that you’ve encountered a complexity merchant.</p><h3>Closing Thoughts</h3><p>I get it: simple, boring, proven platform solutions just aren’t exciting or thrilling to build. They do not create the conditions for those concerned with self-importance, self-preservation, and simultaneously the desire to lord over a fiefdom. If the system is simple, well-documented, and transferable, then anyone can own and operate it, the complexity merchant has no foundation on which to build their empire.</p><p>Why build following boring, tried, true, and established patterns and solutions following paths where the hard challenges have been solved for? Where’s the thrill in that? How shall the moat of self-importance be created if these systems are boring, easy, and straightforward to build upon, operate, and maintain?</p><p>So a word of warning to you, dear traveler: beware of the complexity merchants as you endeavor to create value and seek to gain velocity towards whatever your destination shall be. You will know them by the trail of once new and shiny toys, only half played with, that they leave in their wake — packaging strewn to the side. You will feel the friction that they create and the control that they wish to exert. Do not fall, time and again, for their promises of a Silver Bullet only half kept.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb5ff4128b3d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[On Bakers, Ovens, and AI Startup Moats]]></title>
            <link>https://chrlschn.medium.com/on-bakers-ovens-and-ai-startup-moats-30891abc97be?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/30891abc97be</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Sun, 10 Nov 2024 16:10:01 GMT</pubDate>
            <atom:updated>2024-11-12T13:08:04.879Z</atom:updated>
            <content:encoded><![CDATA[<h3>Summary</h3><ul><li>Many folks who have not worked with LLMs to produce some complex output tend to see most AI startups as simple wrappers around third party models and APIs.</li><li>However, this is the wrong way to think about LLMs and AI. A more accurate analogy is to think of LLMs and AI as an (admittedly magical) oven — an appliance that transforms the input mixture of ingredients into some intermediate product.</li><li>In other words, when evaluating any AI startup, focusing on the oven is the wrong approach. Focus instead on how well the team understands the input ingredients (the data) and how adept they are at preparing that data long before it goes into the “oven” (the LLM).</li><li>Teams that do not invest in deeply understanding, preparing, and structuring the input data sources to the LLM are simply baking supermarket box cake mix; yes of course these are simple wrappers because the effort on the input was low and cheap.</li></ul><h3>“It’s Just Another Wrapper”</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/940/0*s11vGV9lYuHvfscv.png" /><figcaption><em>A misguided response in a Hacker News thread launching the startup Codebuff</em></figcaption></figure><p>In <a href="https://news.ycombinator.com/item?id=42078536">a recent Hacker News thread</a> launching an AI code generation CLI tool <a href="https://chrlschn.dev/blog/2024/11/ai-startup-moats-on-bakers-and-ovens/htt%5Bs://codebuff.com%5D">Codebuff</a>, I saw this comment at the bottom:</p><blockquote><em>I don’t really find it exciting at all, I’m sorry. It’s just another wrapper for a 3rd party model. — Hacker News Poster</em></blockquote><p>This is a sentiment that is quite common to come across in the current tech landscape of AI and LLMs where venture capital has started a gold rush in <em>AI-for-{X}</em> with countless startups in the wake of this boom trying to capitalize. Most early “AI startups” really were just thin wrappers for OpenAI, but — believe it or not — some AI startups really <em>can</em> create a moat and that moat is probably not what you think.</p><p>Before continuing, it is important to differentiate startups that work <em>on</em> AI (and AI adjacent services) — like OpenAI, Anthropic, Mistral, HuggingFace, et al — and startups that work <em>with</em> AI that use LLMs and gen AI to transform and create content (be that text/code, data, images, or video). Here, let us consider only startups that work <em>with</em> AI.</p><h3>Having an Oven Does Not a Baker Make</h3><p>Folks who have not worked heavily with gen AI — day-in, day-out — severely underestimate how difficult it is to get consistently good output from any LLM at the moment. The team at Ownit AI (where I work) first started working with OpenAI around May of 2023 and we’ve spent 18 months learning how to utilize LLMs in an e-commerce context.</p><p>What is clear through the many different iterations of services we’ve built around LLMs is <em>just how lacking they are</em>. Let me iterate the ways:</p><ul><li>Without retrieval augmented generation (RAG), the LLM <em>will</em> hallucinate</li><li>A related problem is that, until web search was integrated, the LLM had no way of knowing about knowledge that came after its training data</li><li>Giving it access to web search creates another problem: search has become “enshittified” with ads and spam results so even giving it access to search can be risky</li><li>Of course, it also won’t have knowledge about information that never showed up in its training dataset or search (e.g. private enterprise data)</li><li>Basic RAG has its limits so teams need to continuously find clever ways to improve the RAG like using graph-based RAG</li><li>The LLM’s understanding of instructions for complex tasks is quite “robotic” in that it prefers following very specific instructions and any ambiguity risks undesired/useless output</li><li>Even when given precise instructions on the output format, LLMs often tend to generate incorrect output formats. This was especially bad with JSON early on, but still present today and crops up when generating output in batches of thousands (you <em>will</em> see failures).</li><li>Even with extremely large context windows, LLMs tend to suffer in their comprehension of the context as the window gets larger so there is a need to create as reasonably focused a context as possible not only for speed and cost, but also for accuracy.</li></ul><p>In almost all cases, the solution to getting the desired output from the LLM is to <em>fix the input</em> by making the prompt and context data more precise, more targeted, or transformed in a way that makes it more likely to get the desired output from the LLM.</p><p>In a sense, this is not unlike <em>baking</em>: the AI is an appliance — the most fantastical and amazing appliance man has made, but yet still an appliance — and the artistry and science all happens <em>before</em> the ingredients go into the oven and we hit “bake”.</p><p>Indeed, there are many “AI startups” that quickly fizzled because <strong><em>having an oven does not a baker make</em></strong> and many startups were just content putting supermarket box mix cake batter into the oven and calling it a day.</p><h3>How AI Startups Build Their Moat</h3><p>In my 20’s, Alton Brown’s <em>Good Eats</em> series was the first time I had the thought that cooking is equal parts art and science. The preparation and combination of ingredients followed by the application of energy (usually in the form of heat or mixing) fundamentally changes the chemistry and physical properties of the ingredients themselves (I actually think it was specifically his episode on mayonnaise and how the different ingredients play their roles in creating an emulsion).</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FL-rLiYl-b1E%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DL-rLiYl-b1E&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FL-rLiYl-b1E%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/4aaf810ae5c7f6a5af82541f9b1be611/href">https://medium.com/media/4aaf810ae5c7f6a5af82541f9b1be611/href</a></iframe><p>Watching the recent Netflix cooking competition <em>Culinary Class Wars</em>, I was often reminded of this as I watched the competitors meticulously transform a set of ingredients into incredibly creative dishes. Every detail from how “Triple Star” (a nickname of one of the competitors) finely sliced wonton wrappers to achieve the correct texture when fried to “Napoli Matfia” creating a Tiramisu that wowed the judges using only ingredients available in a convenience store (even extracting the cream filling from a baked pastry).</p><p>It is really striking just how deeply the top contestants bring not only their culinary skills, but also their deep understanding of the ingredients and how to use each for the desired effect like changing texture, adding acidity, complementing another flavor, masking some pungent note, and so on. In one challenge, the judge notes that “Triple Star” even meticulously chopped vegetables to be the same size as the caviar for more consistent visual presentation and better mouth feel.</p><p>Startups that want to succeed working with AI similarly must understand that <em>the moat is actually in the data</em>:</p><ul><li>How well does the team understand what data is needed and what datasets are available to achieve the desired output.</li><li>How creative the team is in transforming and using that data given the limitations of LLMs.</li><li>How the team thinks about combining different pieces of data to feed context into the LLM.</li><li>How the team puts the finishing touches on the output and presents the results to the consumer (often requiring parsing and transforming once again).</li></ul><p>It is not unlike the culinary arts: the best chefs and bakers deeply understand the input ingredients be it their flavor and texture profiles, how they should be shaped for different applications, how they have to be prepared, how one ingredient will complement another, how to combine it all, and how to present the finished product tastefully and creatively. Hitting “bake” on the oven is simply one step that applies heat to combine and transform the ingredients; all of the artistry and science is actually in the preparation steps before and the presentation steps afterwards.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*p19GY4TQ1z8sMRf0.png" /><figcaption><em>The processes are not dissimilar and it is clear that the LLM is a core part of this process, but perhaps the least important part of the process as far as achieving high quality and contextually relevant output is concerned.</em></figcaption></figure><p>For most AI startups, the moat probably really comes down to how well the team can do ETL and how well the team understands the domain. Does the team have some insight into datasets in a particular domain? Have they come up with a novel or creative way to use that data? Do they understand how to shape and prepare that data given the constraints of LLMs? Can they combine the right pieces of data to achieve the desired output?</p><p>These two videos from Ownit AI’s work with retailer catalogs shows just how good the results can be when the underlying datasets have been extracted, transformed, loaded, and prepared for the LLM.</p><p>In Safeway’s case, the delta between the native Google-powered search results and the Ownit AI generated shelves is striking:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FxQEfo_XCM2M%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DxQEfo_XCM2M&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FxQEfo_XCM2M%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/ae8d7fc8ac1f69a7e3f04e0a140c6a0c/href">https://medium.com/media/ae8d7fc8ac1f69a7e3f04e0a140c6a0c/href</a></iframe><p>In Ulta’s case, the native platform misses entire classes of products when answering the query:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FJL08UDxM_5M%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DJL08UDxM_5M&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FJL08UDxM_5M%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/b4418bd76de7b2d6a3e5bad335a7f881/href">https://medium.com/media/b4418bd76de7b2d6a3e5bad335a7f881/href</a></iframe><p>To achieve both, the process involved extracting and transforming the retailer’s catalog ahead of the selection of the correct context (indeed, it would be impossible to select the correct context without this initial transformation into a graph).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*7ye9nG9TBTTylgh8.png" /><figcaption><em>Much of the magic that enables Ownit to generate superior results is only possible from the acquisition and pre-processing of catalog data into a graph that has been augmented with AI-processed data ahead of time.</em></figcaption></figure><h3>Closing Thoughts</h3><p>What’s clear is that AI startups like Codebuff are anything <em>but</em> just another “wrapper for a 3rd party model”. That third party model is simply an oven; everyone has an oven, but few could claim to be world-class <em>patissiers</em> because at the end of the day, the oven is but an appliance that transforms the input ingredients to the output product. If the input is low effort with cheap ingredients, the chances of the output being high quality and world class — at least with the current magical ovens — are quite slim.</p><p>In the case of Codebuff, the art and science is how the team prepares, resolves, and structures the correct and relevant pieces of the codebase to feed into the LLM; it is not just a simple matter of shoving all of the codebase into the context. For Ownit, it is an analogous challenge (though perhaps a bit more abstract than a codebase) where we’ve invested an incredible amount of effort into acquiring, transforming, and mapping out product catalog data from retailers — classic ETL — so that we can accurately resolve the correct products.</p><p>For both companies — and other AI startups that seek to have a moat — a good portion of the effort and resultant software system is dedicated to designing and building the pipeline that performs the extract-transform-load and the smaller part of the effort is interfacing with the LLM. If you want to build an AI startup moat, everything starts with the ingredients (the data) and how adept you are in your preparation of the data before hitting “bake”.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=30891abc97be" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I’m a Gen AI Maximalist and You Should Be, Too.]]></title>
            <link>https://chrlschn.medium.com/im-a-gen-ai-maximalist-and-you-should-be-too-48a97c87838e?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/48a97c87838e</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[content-creation]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[technology]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Fri, 18 Oct 2024 15:13:33 GMT</pubDate>
            <atom:updated>2024-10-18T15:17:34.537Z</atom:updated>
            <content:encoded><![CDATA[<h3>Summary</h3><ul><li>Content production and distribution has always been constrained by “bandwidth”. For example, the early days of broadcast TV meant that with 3 channels, there could only be 72 hours of programming in a 24 hour day because of limited bandwidth.</li><li>Advancements in technology (e.g. printing press, WWW) have always expanded that bandwidth and along the way, increased the ratio of producers to consumers.</li><li>Generative AI is the next engine for that expansion of bandwidth and it has the potential to push that ratio closer to 1.</li><li>This expansion has always followed a reliable pattern of text → images → audio → video and we see this same pattern with OpenAI and current LLMs.</li><li>We are still quite far from the endpoint and the hype and high valuations are justified; generative AI will create “infinite bandwidth” for content.</li></ul><h3>Are We in the Trough of Disillusionment?</h3><p>Spend enough time on the Internet, and there will be no lack of doubters about the state of AI and specifically generative AI (gen AI). Some bemoan the “AI slop” that has pervaded everything from Facebook feeds to <a href="https://news.ycombinator.com/item?id=41767648">Google Image search results for terms like “baby peacock”</a>. <a href="https://www.reddit.com/r/wallstreetbets/comments/1g57kry/nvidia_is_worth_117_of_the_us_gdp_now_at_the_peak/">Some think that Nvidia’s stock is overvalued</a> and that there is a glut of GPUs — who needs more? Some are already in the trough of disillusionment with AI.</p><p>I beg to differ; <em>I think there’s not nearly enough hype around gen AI and we’re barely getting started</em>.</p><h3>Content and the Concept of “Bandwidth”</h3><p>Some time in the mid 2000’s, I was speaking with an older coworker about <a href="https://en.wikipedia.org/wiki/Current_TV">Al Gore’s TV channel, Current TV</a>. Gore’s Current TV would air pieces produced by citizen journalists; a novel idea at the time. My coworker shared with me an interesting perspective: the history of content, media, and information is one of “bandwidth” — he called it — and Gore’s Current TV represented an expansion of that valuable bandwidth which was once reserved for the likes of Walter Cronkite, Edward R. Murrow, and David Brinkley.</p><p>When broadcast television first started, there were a handful of channels; the bandwidth was limited, he said. Audiences watched more or less the same few shows on the tiny number of channels because there was only so much “bandwidth”. He meant this word in every sense: from the broadcast capacity to the production capacity. 3 channels and 24 hours a day meant, at most, 72 hours of television programming in any given day and there was only so much capacity for professional crews to create that content.</p><p>Then came cable which created more bandwidth and more content followed to fill that capacity. That transition created enough bandwidth to deliver content for a variety of special interests whether that was cooking (Food Network), nature and documentaries (Discovery, History), DIY (HGTV), or even whole channels dedicated to kids shows (Nick, Cartoon Network).</p><p>Of course, the mid 2000’s is also around the time that YouTube started. YouTube represents an <em>exponential</em> explosion in the number of “channels”; it represents <em>seemingly</em> infinite “bandwidth”. There is a channel or a creator for nearly any niche interest one could think of and more content is generated and published every day at an astounding rate; everyone with a phone in their pocket can be a creator.</p><p><em>Aside: it is natural, then, that the overall quality of that content decreases, but there is a broadening of the the space by enabling new producers and publishers.</em></p><p>This same evolution has happened in nearly every content industry from publishing to music to games. At each step of advancement, there is a common pattern: <em>the ratio of producers to consumers increases</em>.</p><p>Viewed from this perspective, gen AI is the <em>next</em> explosion and we are just barely off of the starting line.</p><h3>The Pattern of Advancement</h3><p>Looking at this historically, there has been a repeatable pattern of advancement in terms of the creation and dissemination of content and information. First is always text, then still images. Once we had the ability to reproduce audio and video, then audio and moving images followed text and still images.</p><p>In every era of advancement of content and information, this exact progression has repeated because each step requires more capacity and bandwidth. Think about the early Internet and how it progressed from text to images. Eventually came MP3s and Napster. And finally, we are in the era of YouTube, Zoom, and Netflix; we can start a video call with anyone in the world seemingly free of cost or choose from a catalog of thousands of shows and movies to watch any time we want for less than $20 per month.</p><p>It is important to acknowledge this because I think this gives context to where we are at this moment with gen AI. OpenAI’s ChatGPT first launched with text. Then came <a href="https://openai.com/index/chatgpt-can-now-see-hear-and-speak/">multi-modal models</a>. Most recently, <a href="https://openai.com/index/introducing-the-realtime-api/">OpenAI released their realtime API</a> that supports bi-directional audio streaming. <em>Could it be more obvious what the next leap will be?</em></p><h3>Gen AI as “Infinite Bandwidth”</h3><p>Today, YouTube content already feels infinite; but the ratio of producers to consumers on the platform is still asymmetric and there can only be as much content that is as tailored to a specific audience as there are producers of that content and those producers are constrained by real-life constraints like time and the cost of generating certain types of content such as 3D animated films, for example.</p><p>Gen AI promises something else: a channel of content for every consumer generated specifically for that consumer; <em>“infinite bandwidth”</em>. Effectively, gen AI will accelerate the push of the ratio of producers to consumers ever closer to 1 where every consumer can produce content tailored specific to their niche, interests, preferences, and tastes.</p><p>At <a href="https://ownit.co/">Ownit</a> (where I work), this is precisely the route that we took. In the early days of our efforts with AI (just a year ago 🤣), we were pre-generating content targeting long-tail SEO terms for our retail customers — dozens to thousands of articles at a time.</p><p>After encountering <a href="https://news.ycombinator.com/item?id=40137441">a Hacker News thread on benchmarking LLMs</a> in May, <a href="https://chrlschn.dev/blog/2024/05/need-for-speed-llms-beyond-openai-w-dotnet-sse-channels-llama3-fireworks-ai/">I started to experiment with multi-stream concurrent content generation</a>. The dramatic increase in throughput (this was before GPT-4o) meant that we could deliver all new experiences which were previously unthinkable because of the poor UX caused by low throughput. So instead of pre-generating a set of articles targeted to a set of questions, what if we could literally generate high quality answers to <em>every question</em> on the fly?</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FsUzGeVyiLLg%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DsUzGeVyiLLg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="640" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/abafb8249b26444c88f0ebd35d4102fb/href">https://medium.com/media/abafb8249b26444c88f0ebd35d4102fb/href</a></iframe><p>Of course, for anyone following the industry, Google came to the same conclusion and <a href="https://blog.google/products/search/generative-ai-google-search-may-2024/">released AI Search Overviews around the same time</a>. While Google has taken their punches for some questionable responses from their search overviews, I have no doubt that they are on the right path because it is effectively moving the producer-consumer ratio towards 1: for every question, a custom and tailored chunk of content generated specifically for the consumer to answer their question.</p><h3>Perspective for the Doubters</h3><p>It is easy to be a doubter in this moment because much of the tooling to generate high quality results is still quite niche and complex while often being inaccessible to lay people without the hardware and know-how to pull everything together. The more accessible parts of the ecosystem are producing the low quality content and, frankly, there are a lot of low-quality efforts to leverage gen AI.</p><p>It is easy to look at the low quality, obviously AI-generated content and come to the conclusion that gen AI is a dud; destined to aggravate the <a href="https://en.wikipedia.org/wiki/Enshittification"><em>enshiffitication</em></a> of the once pure web.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1011/0*-5vo0sH3zGIFPK1b.jpeg" /><figcaption>Credit: unknown</figcaption></figure><p>But pay attention and there are some folks out there producing really jaw dropping content and we’re seeing the state of the art advancing quite rapidly. <a href="https://www.reddit.com/r/StableDiffusion/comments/y9zxj1/you_think_thats_insane_heres_what_an_aigenerated/">10 years ago, this was the state of the art research in image generation from text</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/599/1*ZRC9V611H64B5ErZtk5ckg.png" /><figcaption>AI generated images of cows from 2014 research papers.</figcaption></figure><p><a href="https://www.reddit.com/r/StableDiffusion/comments/1f0b45f/flux_is_a_gamechanger_for_character_wardrobe/">And just two months ago</a> (click the link for video):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/810/0*Nr--Lwz2N3XLeKyG.png" /><figcaption>A still from an AI generated movie featuring this scene with a cow.</figcaption></figure><p>Companies like Meta are investing heavily in this space with research projects like <a href="https://ai.meta.com/research/movie-gen/">Movie Gen</a>. It’s obvious to see how this tooling could one day be accessible to anyone right in Instagram or Facebook.</p><p>Reddit’s <a href="https://www.reddit.com/r/aivideo/">/r/aivideo sub</a> is one community to keep an eye on. Examples like this <a href="https://www.reddit.com/r/aivideo/comments/1g5rk0e/womens_fashion_show/">AI generated fashion show</a> or <a href="https://www.reddit.com/r/aivideo/comments/1g5196e/footage_of_the_giza_pyramid_complex_being_built/">fictional depiction of how ancient Egypt was built</a> give a peek into what’s possible and how quickly, week-over-week, the capabilities are advancing.</p><p>Adobe’s Project Turntable is also quite eye opening as the implication is that the underlying model can extrapolate a 3D model from a set of 2D vectors:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fgfct0aH2COw%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dgfct0aH2COw&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fgfct0aH2COw%2Fhqdefault.jpg&amp;key=d04bfffea46d4aeda930ec88cc64b87c&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/c52d1754a4447d7df069dcc64d15db48/href">https://medium.com/media/c52d1754a4447d7df069dcc64d15db48/href</a></iframe><p><a href="https://www.reddit.com/r/singularity/comments/1g2ogq0/counterstrike_runs_purely_within_a_neural_network/">Teams are also experimenting with playable AI generated games</a> (also video):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/810/0*rDoUAMa5Cp4WoP9Q.png" /><figcaption>A playable AI generated version of CS:GO.</figcaption></figure><p>The implications for industries from special effects to film making to game design are immense. They are yet to be disrupted by gen AI, but there is no doubt that as the capabilities expand, accessibility increases, and prices decrease, many industries will have to adjust.</p><h3>What’s at Stake?</h3><p>To understand just how high the stakes are at the moment, the three major cloud providers — Amazon, Google, and Microsoft — have all recently announced investments in nuclear energy to power a massive expansion of compute capacity to fulfill this endpoint of “infinite bandwidth”.</p><p>Here are some headlines from just the last few weeks:</p><ul><li><a href="https://www.ft.com/content/00776191-b010-4104-add4-8dc430386911">Amazon buys stake in nuclear energy developer in push to power data centres</a></li><li><a href="https://www.cbsnews.com/news/amazon-nuclear-reactor-investment-google-kairos-power/">Amazon and Google have plans for fueling their data centers: Nuclear power</a></li><li><a href="https://www.npr.org/2024/09/20/nx-s1-5120581/three-mile-island-nuclear-power-plant-microsoft-ai">Three Mile Island nuclear plant will reopen to power Microsoft data centers</a></li></ul><p>This is a signal that the gen AI era is still early in its infancy. The build-out of this capacity and continued advancements in the algorithms, tooling, and ecosystem will push us closer to that 1:1 ratio of producers to consumers (one might still consume content or a model produced by a creator, but producing a personalized version of it will be a prompt away).</p><p>Meta invested billions in creating the next content platform — virtual reality — only to have the Metaverse largely fizzle. But what if a Star Trek inspired holodeck-lite experience isn’t far in the future? What if the Meta Quest could deliver that virtual experience, tailored to each consumer’s specific interests and tastes? What if it could render any world and any experience from just a few lines of text?</p><p>The stakes are incredibly high because just as Google and Meta became trillion dollar advertising companies with the transition of content consumption from broadcast and cable to the Internet — if the future of content is AI generated — the platform that can deliver this future of content to the consumer will enrich themselves with trillions more.</p><h3>Closing Thoughts</h3><p>While I do not yet have concrete thoughts on artificial general intelligence (AGI) and “singularity”, my framework for thinking about generative AI is drawn from this concept of “bandwidth” that my coworker shared with me almost two decades ago.</p><p>The pattern is obvious: at each stage of bandwidth expansion, the ratio of producers to consumers increases and generative AI seems to be the engine that will push this ratio ever closer to 1. The expansion always moves from text to images to audio to video to interactive. To power this expansion, expect to see continued investments by the likes of Nvidia, Microsoft, Meta, Google, and Apple into generative AI.</p><p>It is easy to look at the middling state of generated content today and dismiss “AI” as a dud; an over-hyped promise that has under-delivered. But history has shown that technology that expands bandwidth for content and decreases the friction for production and distribution — from the printing press to the World Wide Web — will ultimately prevail and deliver riches.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=48a97c87838e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Merging Objects in Google Cloud Storage with Compose and C#]]></title>
            <link>https://itnext.io/merging-objects-in-google-cloud-storage-with-compose-and-c-19879e45af57?source=rss-61db58b8ec88------2</link>
            <guid isPermaLink="false">https://medium.com/p/19879e45af57</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Charles Chen]]></dc:creator>
            <pubDate>Sat, 13 Jul 2024 17:44:39 GMT</pubDate>
            <atom:updated>2024-07-15T12:19:32.415Z</atom:updated>
            <content:encoded><![CDATA[<h3>Summary</h3><ul><li>When working with large dataset ETL, it can be useful to “checkpoint” your output to reduce costs if something fails.</li><li>Google Cloud Storage offers a nifty feature called <a href="https://cloud.google.com/storage/docs/composing-objects#storage_compose_object-csharp">object composition</a> for merging multiple objects into one</li><li>This feature has some limits, but ultimately provides a useful way to perform ETL steps with a “checkpoint” to reduce the cost of recovery.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/959/0*QG-Oey-cW8JsO5W7.png" /></figure><h3>Google Cloud Storage Object Composition</h3><p>Google Cloud Storage has a really nifty feature that allows you to <a href="https://cloud.google.com/storage/docs/composing-objects#storage_compose_object-csharp">“compose” multiple storage objects into one</a>.</p><p>This can be really useful if you’re performing ETL-like tasks across a large dataset and you want to add resiliency to your process by allowing you to “checkpoint” your work by simply dumping out state into a storage object. For example, imagine that you are moving data from an API and processing a large resultant CSV file (think tens of thousands of records) or perhaps processing a stream from a large video file.</p><p>That API call or retrieval may be costly and may be subject to rate limiting. So what happens if your process fails? To solve for this, we can simply periodically dump out a fragment of our extract. For example, imagine that we are extracting 30 days of data from some API. We can simply dump out 1 day at a time and then at the end, merge all 30 days into one large object for the next step of processing.</p><p>Let’s explore how we can achieve this with C# and .NET</p><h3>Getting Set Up</h3><p>First, let’s set up our project:</p><pre>mkdir gcp-storage-compose<br>cd gcp-storage-compose<br>dotnet new console<br>dotnet add package Google.Cloud.Storage.V1</pre><p>Next, you’ll want to grab your account credential key (.json file) for authenticating. As far as I’m aware, there is no storage emulator and the Firebase storage emulator does not support this feature (your API call will fail).</p><h3>Creating the Fragments</h3><p>Let’s start by creating the fragments:</p><pre>using System.Text;<br>using Google.Apis.Auth.OAuth2;<br>using Google.Apis.Storage.v1.Data;<br>using Google.Cloud.Storage.V1;<br><br>var credential = GoogleCredential.FromFile(&quot;credentials.json&quot;);<br>var storage = StorageClient.Create(credential);<br><br>var run = Random.Shared.Next(100, 10000000);<br>var bucket = &quot;__YOUR_BUCKET_HERE__&quot;;<br>var count = 10;<br><br>var fragments = Enumerable<br>  .Range(0, count)<br>  .Select(i =&gt; $&quot;{run}/fragment-{i}.csv&quot;)<br>  .ToArray();<br><br>var tasks = fragments.Select(async path =&gt; {<br>  using var stream = new MemoryStream();<br><br>  var content = Encoding.UTF8.GetBytes(<br>    $&quot;LINE FOR: {path}{Environment.NewLine}&quot;<br>  );<br><br>  await stream.WriteAsync(content);<br>  await storage.UploadObjectAsync(bucket, path, &quot;text/csv&quot;, stream);<br>});<br><br>Task.WaitAll([.. tasks]);</pre><p>The run identifies a unique folder into which we’ll place 10 objects which represent individual fragments of a .csv file.</p><p>Then we can merge it into 1:</p><pre>// Merge into 1<br>await storage.Service.Objects.Compose(<br>  new() {<br>    SourceObjects = fragments<br>      .Select(path =&gt; new ComposeRequest.SourceObjectsData() {<br>        Name = path<br>        })<br>      .ToList(),<br>    Destination = new Google.Apis.Storage.v1.Data.Object {<br>      ContentType = &quot;text/csv&quot;<br>    }<br>  },<br>  bucket,<br>  $&quot;{run}/__merged.csv&quot;<br>)<br>.ExecuteAsync();</pre><p>If we execute this, we’ll see that we generate 10 files and dump out a merged file! Sweet!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/742/0*7HMGLnVYSIx0KBgF.png" /></figure><p>How far can we take this?</p><h3>Probing the Limits</h3><p>Let’s up this to 1500 objects:</p><pre>var count = 1500;</pre><p>And our first stumbling block:</p><pre>Unhandled exception. The service storage has thrown an exception.<br>HttpStatusCode is BadRequest.<br>Google.Apis.Requests.RequestError<br>The number of source components provided (1500) exceeds the maximum (32) [400]<br>Errors [<br>  Message[The number of source components provided (1500) exceeds the maximum (32)] Location[ - ] Reason[invalid] Domain[global]<br>]</pre><p>So the maximum number of individual items we can compose is limited to 32.</p><p>We’ll need to change how we do the composition into chunks.</p><h3>Refactoring for Chunking</h3><p>Let’s first extract a function that encapsulates the composition:</p><pre>var compose = (string[] paths) =&gt; {<br>  storage.Service.Objects.Compose(<br>    new() {<br>      // 👇 Always stuff the first fragment into the paths<br>      SourceObjects = ((string[])[fragments[0], .. paths])<br>        .Select(path =&gt; new ComposeRequest.SourceObjectsData() {<br>          Name = path<br>        })<br>        .ToList(),<br>      Destination = new Google.Apis.Storage.v1.Data.Object {<br>        ContentType = &quot;text/csv&quot;<br>      }<br>    },<br>    bucket,<br>    fragments[0] // 👈 Overwrite the first object<br>  )<br>  .Execute();<br>};</pre><p>Our strategy is simple: whatever the first fragment is, we’ll just compose everything into it iteratively.</p><p>Now we need to aggregate our fragments into one:</p><pre>fragments<br>  .Skip(1)            // Skip the first one since we&#39;ll always insert this.<br>  .Chunk(31)          // Take in chunks of 31 since we&#39;ll always insert one.<br>  .ToList()           // Get all of our chunks<br>  .ForEach(compose);  // Compose each chunk in turn</pre><p>And here is our output file:</p><pre>LINE FOR: 1729868/fragment-0.csv<br>LINE FOR: 1729868/fragment-1.csv<br>LINE FOR: 1729868/fragment-2.csv<br>LINE FOR: 1729868/fragment-3.csv<br>LINE FOR: 1729868/fragment-4.csv<br>LINE FOR: 1729868/fragment-5.csv<br>....<br>LINE FOR: 1729868/fragment-37.csv<br>LINE FOR: 1729868/fragment-38.csv<br>LINE FOR: 1729868/fragment-39.csv</pre><p>Nice!</p><p>With this mechanism in place, then we can easily “checkpoint” and dump out a partial CSV as we process a large dataset.</p><p>If we need to restart, simply check if the dataset already exists in Cloud Storage and add the path if it does. Otherwise, process the chunk and dump out a CSV.</p><p>If we fail at row 40,000 of 100,000, we don’t have to start at 0 and instead, we can just pick up at 40,001 instead.</p><p>Then on a successful run, merge everything into the first file.</p><h3>Deleting the Excess Objects</h3><p>Once we’ve merged the objects, we can actually simply delete them.</p><p>Let’s add that in:</p><pre>var compose = (string[] paths) =&gt; {<br>  storage.Service.Objects.Compose(<br>    new() {<br>      SourceObjects = ((string[])[fragments[0], .. paths])<br>        .Select(path =&gt; new ComposeRequest.SourceObjectsData() {<br>          Name = path<br>        })<br>        .ToList(),<br>      Destination = new Google.Apis.Storage.v1.Data.Object {<br>        ContentType = &quot;text/csv&quot;<br>      }<br>    },<br>    bucket,<br>    fragments[0]<br>  )<br>  .Execute();<br><br>  // 👇 We can delete concurrently.<br>  var deletionTasks = paths.Select(<br>    path =&gt; storage.DeleteObjectAsync(bucket, path)<br>  );<br><br>  Task.WaitAll([.. deletionTasks]);<br>};</pre><p>Now our contents after 1 run:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/742/0*x71SfbttmvIDA3vV.png" /></figure><p>Only one file remains; keeping everything tidy.</p><h3>Limited Chunks</h3><p>What if we actually don’t want 1 massive file? What if we want to compose say every <em>n</em> files in to 1? An example might be a case where each file has 1,000 records and we want resultant files to max out at 10,000 records.</p><p>Let’s update our logic here so that we dump out a file for each subset:</p><pre>var compose = (string[] paths) =&gt; {<br>  storage.Service.Objects.Compose(<br>    new() {<br>      SourceObjects = paths // 👈 No longer inserting here<br>        .Select(path =&gt; new ComposeRequest.SourceObjectsData() {<br>          Name = path<br>        })<br>        .ToList(),<br>      Destination = new Google.Apis.Storage.v1.Data.Object {<br>        ContentType = &quot;text/csv&quot;<br>      }<br>    },<br>    bucket,<br>    paths[0] // 👈 Compose into the first file<br>  )<br>  .Execute();<br><br>  var deletionTasks = paths<br>    .Skip(1) // 👈 Skip the first file for deletion<br>    .Select(<br>      path =&gt; storage.DeleteObjectAsync(bucket, path)<br>    );<br><br>  Task.WaitAll([.. deletionTasks]);<br><br>  return paths[0]; // 👈 Return the path so we can print it<br>};<br><br>var chunkSize = 10;<br><br>var paths = fragments<br>  .Chunk(chunkSize) // 👈 No skip here<br>  .ToList()<br>  .Select(compose);<br><br>foreach (var path in paths) {<br>  Console.WriteLine(path);<br>}</pre><p>Here is our output:</p><pre>➜  gcp-storage-compose dotnet run<br>5056692/fragment-0.csv<br>5056692/fragment-10.csv<br>5056692/fragment-20.csv<br>5056692/fragment-30.csv<br>Completed run: 5056692</pre><p>And in storage:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/740/0*HU9d7GOFc7vSITe5.png" /></figure><p>The contents for the first chunk:</p><pre>LINE FOR: 5056692/fragment-0.csv<br>LINE FOR: 5056692/fragment-1.csv<br>LINE FOR: 5056692/fragment-2.csv<br>LINE FOR: 5056692/fragment-3.csv<br>LINE FOR: 5056692/fragment-4.csv<br>LINE FOR: 5056692/fragment-5.csv<br>LINE FOR: 5056692/fragment-6.csv<br>LINE FOR: 5056692/fragment-7.csv<br>LINE FOR: 5056692/fragment-8.csv<br>LINE FOR: 5056692/fragment-9.csv</pre><p>And the second:</p><pre>LINE FOR: 5056692/fragment-10.csv<br>LINE FOR: 5056692/fragment-11.csv<br>LINE FOR: 5056692/fragment-12.csv<br>LINE FOR: 5056692/fragment-13.csv<br>LINE FOR: 5056692/fragment-14.csv<br>LINE FOR: 5056692/fragment-15.csv<br>LINE FOR: 5056692/fragment-16.csv<br>LINE FOR: 5056692/fragment-17.csv<br>LINE FOR: 5056692/fragment-18.csv<br>LINE FOR: 5056692/fragment-19.csv</pre><p>Nice; we can imagine that each of these files had 1,000 rows and now file 1 and file 2 have 10,000 rows each.</p><h3>Closing Thoughts</h3><p>If you’re in Google Cloud, this is a neat platform feature that allows you to do interesting things with your ETL type workloads without involving a database for what is ostensibly temporary data.</p><p>By check pointing each set of data, it’s possible to avoid potentially costly retries.</p><h3>Full Code Listing</h3><pre>using System.Text;<br>using Google.Apis.Auth.OAuth2;<br>using Google.Apis.Storage.v1.Data;<br>using Google.Cloud.Storage.V1;<br><br>var credential = GoogleCredential.FromFile(&quot;credentials.json&quot;);<br>var storage = StorageClient.Create(credential);<br><br>var run = Random.Shared.Next(100, 10000000);<br>var bucket = &quot;__YOUR_BUCKET_HERE__&quot;;  // 👈 Replace this<br>var count = 40;<br><br>var fragments = Enumerable<br>  .Range(0, count)<br>  .Select(i =&gt; $&quot;{run}/fragment-{i}.csv&quot;)<br>  .ToArray();<br><br>var tasks = fragments.Select(async path =&gt; {<br>  using var stream = new MemoryStream();<br>  var content = Encoding.UTF8.GetBytes($&quot;LINE FOR: {path}{Environment.NewLine}&quot;);<br>  await stream.WriteAsync(content);<br>  await storage.UploadObjectAsync(bucket, path, &quot;text/csv&quot;, stream);<br>});<br><br>Task.WaitAll([.. tasks]);<br><br>var compose = (string[] paths) =&gt; {<br>  storage<br>    .Service.Objects.Compose(<br>      new() {<br>        SourceObjects = paths<br>          .Select(path =&gt; new ComposeRequest.SourceObjectsData() {<br>            Name = path<br>          })<br>          .ToList(),<br>        Destination = new Google.Apis.Storage.v1.Data.Object {<br>          ContentType = &quot;text/csv&quot;<br>        }<br>      },<br>      bucket,<br>      paths[0]<br>    )<br>    .Execute();<br><br>  var deletionTasks = paths<br>    .Skip(1)<br>    .Select(<br>      path =&gt; storage.DeleteObjectAsync(bucket, path)<br>    );<br><br>  Task.WaitAll([.. deletionTasks]);<br><br>  return paths[0];<br>};<br><br>var chunkSize = 10;<br><br>var paths = fragments<br>  .Chunk(chunkSize)<br>  .ToList()<br>  .Select(compose);<br><br>foreach (var path in paths) {<br>  Console.WriteLine(path);<br>}<br><br>Console.WriteLine($&quot;Completed run: {run}&quot;);</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=19879e45af57" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/merging-objects-in-google-cloud-storage-with-compose-and-c-19879e45af57">Merging Objects in Google Cloud Storage with Compose and C#</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>