Skip to content

Add built-in filesystem tools and complete SDK size optimizations - #3939

Open
vigoo wants to merge 92 commits into
mainfrom
gol-314-filesystem-followups
Open

vigoo wants to merge 92 commits into
mainfrom
gol-314-filesystem-followups

Conversation

@vigoo

@vigoo vigoo commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Rust and MoonBit implementations of the built-in read-file, write-file, and edit-file tools, alongside WASM size optimizations across the Rust, MoonBit, Scala, TypeScript, and Effect SDKs.

The filesystem components shrink from 1,219,887 B to 394,326 B for Rust (67.7%) and 1,070,119 B to 672,478 B for MoonBit (37.2%), while retaining the existing five-interface export contract.

The main changes operate at four distinct layers:

  • Compile-time specialization: generate concrete descriptors, dispatch, and clients instead of interpreting schemas at runtime.
  • Dead-code elimination: stop unused capabilities and reflection from retaining their implementation stacks.
  • Direct wire encoding: avoid the owned schema model on ordinary generated invocation paths.
  • Canonical-ABI deduplication: share necessary lifting, lowering, and cleanup code in wit-bindgen rather than emitting copies per export.

Filesystem-tool scope

  • Adds Rust and MoonBit read-file, write-file, and edit-file components.
  • Provisions all six suffixed built-in tool releases idempotently, allowing both implementations to be evaluated.
  • Adds real-worker coverage for sandbox filesystem behavior, guest invocation, snapshot recovery, and registry provisioning.

How the size reductions work

1. Specialize at generation time instead of interpreting at runtime

Generated code already knows the command tree, argument types, result types, and error declarations. Previously, ordinary generated paths still used general-purpose builders and dispatch machinery to rediscover that information at startup or on each invocation.

The generators now emit:

  • Final wire descriptors for schemas, command trees, metadata, and errors.
  • Direct command-path matches and concrete argument bindings.
  • Typed clients with precomputed command paths, input layouts, output shapes, and error decoders.

For example, an illustrative files/read(path) command changes from:

Before: find command in tree → inspect input schema → project "path" → call handler
After:  generated path match → decode the known "path" field        → call handler

Arity, shape, command identity, and protocol checks remain. What disappears is the generic machinery for discovering how to perform them.

Where registration still requires descriptor construction or validation, the result is cached instead of reconstructed and revalidated on every call. MoonBit and Scala code generation also resolve capability hooks and binding information during compilation rather than retaining broader runtime discovery paths.

2. Make unused capabilities and reflection removable

An unused feature is not free if an export, registration function, or package import keeps its runtime reachable.

Rust and MoonBit now use small canonical-ABI entrypoints for absent capabilities. The required exports remain present, but their full typed dispatchers are linked only when an implementation exists. Rust registration constructors install those dispatchers for concrete implementations rather than rooting them from common SDK code.

For example, a component with no tool implementation still supplies the required tool exports, but does not need the typed tool-dispatch stack behind them. The minimal exports still obey allocation, post-return, and async task-return ownership rules.

TypeScript and Effect address the same retention problem through capability-specific bundle entrypoints, separate reflection entrypoints, and narrow side-effect declarations. Generated Rust clients no longer query agent types merely to construct a typed client.

Explicit dynamic and reflected APIs remain available. Ordinary typed clients no longer pull them in accidentally.

3. Encode directly at the wire boundary

Specialized dispatch alone is insufficient if every argument and result still passes through a generic owned representation.

Generated invocation codecs now read and write the WIT wire arena directly, bypassing the intermediate SchemaGraph, SchemaValue, and TypedSchemaValue model.

For an illustrative record value:

Before: { path, contents } → owned schema/value model → WIT wire arena
After:  { path, contents } → generated encoder        → WIT wire arena

The same principle extends beyond basic invocation arguments:

  • Tool composition and middleware projection use wire descriptors and typed invocation boundaries, preserving subtree forwarding, aliases, and custom errors.
  • Generated guests perform focused structural checks; rich graph validation remains available to host and dynamic APIs.
  • Snapshots and restore context use narrow typed JSON/principal codecs, and built-in string errors are encoded directly into their wire representation.

These changes remove otherwise independent retention paths into generic model, validation, and JSON-value machinery. They do not remove JSON where the protocol requires it or where application code uses it.

Resource ownership remains part of the wire boundary: encoding performs a resource-only preflight before transferring handles, decoding tracks ownership in the arena, and failure/cancellation paths clean up resources still owned by the guest. This covers resource-bearing values such as streams, secrets, and quota tokens.

4. Share live canonical-ABI code in wit-bindgen

Even after unused SDK code is eliminated, active exports still need canonical-ABI lifting, lowering, allocation, resource transfer, and cleanup.

wit-bindgen previously emitted similar nested walkers separately for exports using the same resource-bearing shapes. The generator now outlines reusable helpers, including async lowering and post-return reclamation.

Before: export A → its own walker for shape T
        export B → another walker for shape T

After:  exports A and B → shared walker for shape T

This is distinct from dead-code elimination: the walkers are needed, but their implementations no longer need to be duplicated. Live tag dispatch, nested allocation/copy, resource transfer, and reclamation remain.

Additional runtime improvement: MoonBit provider stdout now writes in bounded 64 KiB windows, preserving partial writes, stream errors, and finish/drop behavior while reducing boundary-crossing overhead.

Size results

Rust release artifacts now consistently strip symbol/debug residue, and size-analysis fixtures record reproducible build settings across SDKs.

Filesystem components

Both implementations retain the same five exported interfaces.

Implementation Initial stripped component Final committed component Reduction
Rust 1,219,887 B 394,326 B 67.7%
MoonBit 1,070,119 B 672,478 B 37.2%

The final Rust component is 41.4% smaller than the final MoonBit component.

Isolated effect of shared ABI walkers

Controlled before/after measurements, in stripped bytes:

Fixture Before outlining After outlining
Typed-tool floor, full export contract 166,453 B 150,652 B
Tool-only control 135,973 B 120,164 B
Rust filesystem 491,124 B 394,326 B
MoonBit filesystem core module 647,648 B 607,644 B

The tool-only fixture is a measurement control, not a change to the shipped export contract. The MoonBit row measures a core module, not the complete component in the preceding table.

The ≤170 KB typed-tool-floor target was met. The aspirational ≤350 KB Rust filesystem target was missed by 44,326 B.

Twiggy attribution identifies live tag dispatch, nested allocation/copy, resource transfer, and reclamation in the remaining shared helpers—not duplicated export walkers. Table-driven canonical-ABI interpretation or a different representation/contract are possible directions for further investigation, not changes implemented here.

Capability-free SDK overhead

The corrected empty Rust fixture uses export_golem_component!() without registering an implementation:

  • Capability-free SDK component: 69,890 B stripped.
  • Pure wit-bindgen control with the same export contract: 69,730 B.
  • Measured difference: 160 B.

The previous “empty” fixture registered a durable agent and therefore also measured registry, snapshot, principal, and JSON behavior.

Worker performance

Optimized-host measurements for 64 KiB files, with 100 samples after 10 warmups:

Operation Rust median (p95), ms MoonBit median (p95), ms
Write 4.854 (5.172) 5.520 (6.492)
Read 5.102 (5.994) 5.488 (7.210)
Edit 6.516 (8.125) 6.816 (8.260)

These include worker dispatch, component execution, and sandbox filesystem overhead. The linked benchmark executable was invoked directly, so compilation is excluded.

These timings predate the final size follow-ups. Final artifacts were revalidated with real-worker filesystem tests, but the 100-sample latency matrix was not rerun. This is a latency comparison between the measured implementations, not a before/after speedup measurement for the optimization work.

Validation and limitations

Behavior, ABI, and retention

  • Rust and MoonBit filesystem WASMs validate, retain the expected five-interface export contract, and introduce no new imports.
  • Real-worker filesystem behavior, guest invocation, snapshot recovery, and registry provisioning/idempotency checks pass.
  • Raw absent-interface ABI and resource-ownership checks pass across all capability combinations.
  • Size-analysis tests and component/export-contract checks pass.

CI retention checks inspect every embedded core module in the checked fixtures using wasm-tools, demangling, and Twiggy evidence. Empty and ordinary typed fixtures exclude forbidden generic model/validation families and reflection imports. A positive reflection fixture retains the expected host import and symbols, checking that the negative tests can actually detect those dependencies.

SDK checks

  • Rust: macro, SDK, and filesystem tests; formatting; scoped Clippy with -D warnings pass.
  • MoonBit: SDK, codegen, direct-wire, reflection, and ownership tests, plus deterministic regeneration, pass.
  • Scala: macro and codegen suites pass (131 + 170 tests).
  • TypeScript: 1,010 tests pass, 20 skipped; lint has no errors; bundle checks confirm ordinary paths omit generic models.
  • Effect: 986 tests, typecheck, lint, and formatting pass.

Remaining validation gaps

  • Scala: the broader test-agent link remains unverified because this checkout lacks the generated golem/wasm/agent_guest.wasm prerequisite.
  • MoonBit: the white-box filesystem runner lacks the WASI 0.3 future-drop-readable intrinsic. Committed components validate and the real-worker filesystem tests pass.

Dependencies

vigoo and others added 28 commits September 21, 2026 12:06
Amp-Thread-ID: https://ampcode.com/threads/T-01a0c36a-7622-747a-80f5-f801d67a09c0
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	sdks/moonbit/golem_sdk/wit/main.wit
…ructors

Keep mandatory agent, tool, middleware and snapshot exports in one guest world, dispatching absent capabilities without retaining their runtimes. Register immutable prepared tool descriptors once and test native behavior, linked exports, metadata discovery and runtime retention across five capability combinations.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c98e-4575-705f-8dcf-295fa7aac1b4
Co-authored-by: Amp <amp@ampcode.com>
…tracts

Integrate the schema feature work from 95e3fb2cc475a27d9a3bedfee30d2255548ce8c0 while retaining immutable prepared tool descriptors rather than Rc lookup. Validate all five capability combinations with lean and rich-validation guests, and refresh streaming fixture lockfiles through the CLI rebuild.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c98e-4575-705f-8dcf-295fa7aac1b4
Co-authored-by: Amp <amp@ampcode.com>
Pin wit-bindgen 9c6dce16 and regenerate the full guest world through the SDK script. Use its bounded 64 KiB callback window for handwritten ProviderStdout, retain multi-chunk drain coverage, and assert the real-tool burst submission count.

The MoonBit generator change batches byte streams but does not share export-result lowering across interfaces. Keep the same-world minimal export implementation in the preceding commit.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c98e-685a-73ca-819c-142fb75f68d6
Co-authored-by: Amp <amp@ampcode.com>
Select full agent, tool and middleware implementations from registered source capabilities while retaining minimal mandatory exports for absent capabilities. Keep the full guest ABI and allow Scala.js to eliminate unused dispatch and host imports.

Cover all capability combinations, projected/client-only tools, linked entry discovery and invocation, snapshot error behavior and typed client RPC through a host stub. Document reproducible bundle/component size and boundary latency measurements.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c98e-7223-7153-b12c-ef1aa822aedc
Co-authored-by: Amp <amp@ampcode.com>
Keep the single full guest world while moving SDK reachability into the application bundle. Select static empty exports for absent capabilities and preserve synchronous discovery after module initialization.

Cover empty, tool-only, agent-only, middleware-only and mixed bundles, opaque registration helpers, tool invocation and agent snapshot roundtrips. Add a release size and preinitialization measurement harness.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c98e-8011-70cd-a2ea-668ac1be48da
Co-authored-by: Amp <amp@ampcode.com>
Add Rust and MoonBit read/write/edit filesystem tool components, provision their releases, and cover direct and guest-invoked behavior. Integrate capability-sensitive SDK exports, bounded MoonBit stream batching, shared cross-interface ABI helpers, release stripping, reproducible size analysis, and final performance reports while preserving the unified full world.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c87f-eeaa-77b9-a78f-21483cb9b54d
Co-authored-by: Amp <amp@ampcode.com>
Retain the unified five-interface export contract while linking typed lifting and lowering only for registered capabilities. Generate MoonBit reexports from source capability discovery and cover canonical ownership, post-return, async signatures and structural WIT contracts.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0cd68-5968-745a-9d12-522e1ab909af
Co-authored-by: Amp <amp@ampcode.com>
Convert concrete values without owned schema models, with resource preflight and affine transfer. Cover nested values, structural errors, rich fields, result payloads, derive hygiene, and skipped generic fields. Generated tool and agent invocation integration remains separate.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0cda0-794b-75c8-a7c7-2de98adf1682
Co-authored-by: Amp <amp@ampcode.com>
Serialize typed version/principal envelopes and retain raw JSON state. Decode snapshot principals directly from tagged raw payloads while retaining the generic Principal serde API for dynamic callers. Cover old envelope semantics, exact canonical bytes, field ordering, nulls, and invalid versions.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0cda0-794b-75c8-a7c7-2de98adf1682
Co-authored-by: Amp <amp@ampcode.com>
Avoid building and validating a generic schema graph for statically known string errors. This removes the Empty fixture validation root while preserving its custom-error wire payload.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0cda0-794b-75c8-a7c7-2de98adf1682
Co-authored-by: Amp <amp@ampcode.com>
Base automatically changed from gol-497-single-sdk-world to main September 23, 2026 11:40
Use transactional wire ownership for generated dispatch, RPC clients, typed streams, configuration, and tool results. Keep model-based APIs for dynamic consumers. Generated tool input dispatch and tool descriptors remain to be specialized.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0cdf0-c7cd-724a-9c3d-0d203160a4f9
vigoo and others added 21 commits September 24, 2026 19:23
Regenerate the committed Rust and MoonBit filesystem tool components with the current SDK contracts and ensure their CI builder installs protoc for the golem binary dependency graph.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0c87f-eeaa-77b9-a78f-21483cb9b54d
Use the direct-wire and configuration derives required by the optimized Rust SDK for the secret-policy caller component.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0c87f-eeaa-77b9-a78f-21483cb9b54d
Update the standalone fixture lockfile to the current Rust SDK dependency graph and pinned wit-bindgen revision.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a0c87f-eeaa-77b9-a78f-21483cb9b54d
…llowups

Amp-Thread-ID: https://ampcode.com/threads/T-01a0c87f-eeaa-77b9-a78f-21483cb9b54d
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	golem-worker-executor/src/services/worker/session_index_tests.rs
#	sdks/moonbit/golem_sdk/config/pkg.generated.mbti
#	sdks/moonbit/golem_sdk/config/top.mbt
#	sdks/ts/packages/golem-ts-sdk/src/secret.ts
@vigoo
vigoo marked this pull request as ready for review September 25, 2026 14:28
@vigoo
vigoo requested a review from a team September 25, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants