TS SDK redesign#3680
Conversation
Phase 0 (bugfix + async init): - FluentAgentThis: fix phantomId() -> getPhantomId() (matched the runtime wiring), add getPrincipal(); runtime wires getPrincipal from the init principal. - Allow async init: widen AgentInitiator.initiate/initiateFromWit to return Result | Promise<Result>; the guest initialize + load-snapshot paths await it (backward-compatible with the decorator SDK's sync initiate). Drop the async-init rejection in the fluent initiator; await impl.init. Phase 2 (schema walker, structural kinds): extend the Zod walker to tuple, enum, literal (-> base primitive / enum), z.record -> map, z.map, and discriminated union -> variant (handling the Zod v4 quirk where a discriminated union reports type:'union' with a discriminator). Plain z.union throws clear guidance pending robust structural matching / variant markers. Tests: 13 fluent walker/defineAgent tests pass (incl. async-init + the new kinds).
Phase 2 — vendor-neutral marker schemas (src/fluent/schema/markers.ts):
- A marker is a valid StandardSchemaV1 carrying a hidden WIT_MARKER brand (a
MarkerDescriptor thunk (recurse)->FluentCodec); compileSchema intercepts branded
schemas before vendor dispatch. Namespace `s`:
- scalar pins s.u8..s.s64/s.f32, s.char, s.datetime, s.duration, s.url, s.bytes
- capability nodes s.secret(inner) (GuestSecretHandle), s.quotaToken()
(GuestQuotaTokenHandle), s.unstructuredText/Binary (variant{inline,url}+role),
s.multimodal(cases) (list<variant>+role) — value logic ported from effect-golem.
Phase 3 — AgentType metadata flow (defineAgent/method/runtime):
- AgentSpec: description/promptHint/mode(durable|ephemeral)/dependencies/snapshotting.
- MethodSpec: description/promptHint/readOnly.
- assembleAgentType threads these into the WIT agent-type (mode, snapshotting variant,
per-method read-only-config, agent-dependency records from registered dep AgentTypes);
defaults preserve prior behavior.
Tests: 33 fluent tests pass (26 walker/marker + 7 metadata). src/fluent type-clean.
…lkers
Phase 4 (config): src/fluent/config.ts — ConfigSpec {local?, secret?}; declarations
emitted into the agent-type (local -> value-type, secret -> secret<inner>); runtime
`this.config`/InitContext.config accessor reads getConfigValue and reveals secrets via
golem:secrets/reveal.
Phase 5 (RPC): src/fluent/client.ts — clientFor(def)(id) returns a typed proxy that
calls a remote agent; encodes via the LOCAL def's FluentCodecs (symmetric with the
exported invoke path), reusing host WasmRpc/makeAgentId/awaitPollable. + .trigger/.schedule.
Phase 7 (typed surfaces): src/fluent/{keyvalue,blobstore,websocket}.ts — de-Effect-ified
ports (plain async, thrown typed errors, no Layer/Scope); forSchema(std) over JSON byte
payloads. (RDBMS deferred.)
Phase 8 (multi-vendor): src/fluent/schema/{valibot,arktype,effect}.ts — Standard-Schema
walkers duck-typing each vendor's internals (never importing the lib), mirroring zod.ts.
Integration: index.ts wires the new exports + walker side-effect registrations; vitest
aliases the new host bindings so the barrel loads in node; the schema/agent/config tests
import specific submodules to avoid pulling host-binding surfaces.
Tests: 71 fluent tests pass (walker/markers 26, agent 7, config 3, vendors 35); the
19 fluent-io surface tests are skipped under the global host-binding alias and validated
in the playground. Decorator suite still loads the barrel (verified). src/fluent type-clean.
Phase 6 (HTTP): src/fluent/http.ts — mount/endpoint specs (path templates + builders,
verb shorthands, auth/cors/webhook-suffix); compileMount/compileEndpoint emit the WIT
http-mount-details/http-endpoint-details, reusing the decorator path/query parsers
(src/internal/http/{path,query,validation}.ts). defineAgent.http + method.http flow into
assembleAgentType (was hardcoded undefined/[]); registry-free consistency validation.
Phase 7d (RDBMS): src/fluent/rdbms/{shared,postgres,mysql,ignite,index}.ts — Postgres/
MySQL/Ignite as plain-async drivers (open/query/execute/transaction/close), full
db-value<->JS codecs ported from effect-golem (host-import-free in shared.ts), rich param
helpers, typed errors. (Streaming deferred.)
Integration: index.ts exports http + rdbms; vitest aliases the golem:rdbms bindings.
Tests: 103 fluent tests pass; 20 host-binding tests skipped under the global alias
(validated in the playground). src/fluent type-clean.
- rollup external: externalize all golem:*/wasi:* specifiers (plus agent-guest/ node:sqlite) via a predicate, so the new fluent host surfaces (keyvalue/blobstore/ websocket/rdbms) and their bindings aren't bundled. - rollup-plugin-typescript2 check:false (transpile-only): the decorator-era files (agentConfig/mapping/typegen) have pre-existing type errors from the new schema-model (Opaque secrets #3674) that block the build; they're erased at runtime and unused by fluent agents. Type-checking still runs via `tsc --noEmit`. Produces dist/index.mjs + the agent_guest.wasm embedding the fluent runtime.
…ckJS)
The keyvalue/blobstore forSchema decoders used `new TextDecoder('utf-8', {fatal:true})`
at module scope, which throws at load in the QuickJS agent guest (compiled without ICU),
trapping module init. Add a strictTextDecoder() helper (try fatal / fall back lenient)
and use it in both surfaces.
src/index.ts only re-exported defineAgent/method/registerSchemaWalker from the fluent barrel, so clientFor, the markers `s`, the typed host surfaces (keyvalue/blobstore/ websocket/rdbms), and the `http` helpers weren't importable from '@golemcloud/golem-ts-sdk'. Replace the narrow re-export with `export * from './fluent'`.
The current wasm-rquickjs wrapper looks up the guest export by the WIT interface
short name (`guest.discoverAgentTypes` of golem:agent/guest@2.0.0), not the
package-qualified `golemAgent200Guest`. A freshly built agent_guest.wasm therefore
trapped at discover-agent-types ("Cannot find exported JS function guest...").
Export `guest`/`tool` as aliases of golemAgent200Guest/golemTool010Guest so the
generated wrapper resolves them. (Same convention effect-golem uses.)
After merging main (numeric type nodes carry option<numeric-restrictions>):
- markers: s.u8..s64/f32 accept an optional { min?, max?, unit? } and emit
t.<kind>(restrictions) with the right NumericBound tag (unsigned for uN, signed
for sN, float-bits for fN); the validator tightens to the user's bounds.
- zod walker: z.number().min/max (v3 _def.checks / v4 _zod.def.checks) -> f64
float-bits restrictions; plain numbers stay unconstrained.
Tests: 31 fluent tests pass (+5 numeric-restriction round-trip/shape cases).
✅ Deploy Preview for golemcloud canceled.
|
Adds a `durable(spec, request, body)` helper (and a `FunctionType` set mirroring the WIT wrapped-function-type) in the shared host/ layer, so both the fluent and decorator surfaces gain it. Wraps a non-deterministic side effect so its typed result is persisted to the oplog on the live run and replayed — without re-running the body — during recovery. - Built over the golem:durability@1.5.0 host interface (already in the world, previously unused) + the fluent schema codec + the Item-3 result codec (typedSchemaValueToWit / typedSchemaValueFromWit round-trip). - Result-value shape: with an `error` schema the body returns a Result<S,E> and a WIT result<S,E> is persisted/replayed; without it the body returns the success value directly and a throw is an uncaught defect. - Protocol mirrors effect's Durability.wrap: observe -> begin -> current-state -> live (run under persist-nothing, persist, end) / replay (read, validate function name + type, decode, end). Tests: tests/fluent-durable.test.ts (live persist, replay-without-rerun, fallible ok/err, async) via the vi.doMock + dynamic-import pattern; adds the golem:durability test alias. Demo: fluent-demo durability-agent wrappedQuote / wrappedQuoteFailing.
…pshots
Snapshotting was policy-only on the surface but reflected over ALL of `this`
(untyped, and it serialized + clobbered the live `config` accessor). This adds
the typed/custom paths that effect has and hardens the default:
- `snapshotting: { state: <schema>, policy }` — the JSON snapshot is scoped to
and validated by the schema; only the declared state fields of `this` are
persisted/restored (drops config, helpers, stray fields). A shape mismatch on
load throws. Reflective default kept when `state` is omitted (back-compat).
- `implement({ snapshot: { save, load } })` — custom, `this`-bound serialization
that overrides the default entirely (parity with the decorator SDK's
BaseAgent.save/loadSnapshot and effect's Snapshot.custom).
- Reflective default no longer serializes the live `config` accessor; instanceof
checks tolerate an absent node:sqlite constructor.
Tests: tests/fluent-snapshot.test.ts (typed scoping, schema round-trip, invalid
rejection, config not serialized, custom bytes). Demo: fluent-demo Counter uses
`snapshotting: { state, policy: everyNInvocations }`.
Naming alignment (Item 11). Governing rule from ecosystem research: align TERMS to the clearer word, but keep case-style + grouping per-subecosystem. For the saga/compensation concept effect's term is the precise one (it's a saga, not an ACID transaction), so fluent adopts it — as camelCase free-functions (fluent's idiom), not effect's namespace. src/host/transaction.ts -> src/host/saga.ts: - fallibleTransaction / infallibleTransaction -> fallibleSaga / infallibleSaga - operation / Operation -> compensable / Compensable - TransactionResult / TransactionFailure -> SagaResult / SagaFailure - FallibleTransaction / InfallibleTransaction -> FallibleSaga / InfallibleSaga - OperationErrors -> CompensableErrors Prose updated to saga/step vocabulary; runtime behaviour unchanged. The decorator surface does not use these; effect already uses `Saga`. 658 tests green; demo saga-agent redeployed and invoked (commit / rollback / infallible all correct).
…builders
Agent-management breadth (Item 9). fluent already had the capabilities
(updateAgent/forkAgent/revertAgent/fork/resolveAgentId/getSelfMetadata/GetAgents/
promises/oplog); the gap vs effect was ergonomic. Added in the flat host/ style:
- Filter DSL — Filter.{name,status,version,createdAt,env,config} + .and/.or,
compiled to the WIT agent-any-filter via disjunctive normal form
((A or B) and C -> (A and C) or (B and C)).
- getAgents(componentId, filter?, precise?) — a generator that accepts a Filter
(or raw AgentAnyFilter) and lazily pages the host get-agents resource, yielding
enriched AgentMetadata.
- RevertTarget.toOplogIndex(i) / .lastInvocations(n) builders for revertAgent.
Skipped effect's typed-error/Stream/Promises-namespace conveniences (they fight
fluent's plain-throw flat idiom). No rename: hostapi stays flat — this also
settles the Item 11 hostapi/Agents naming (terms already agree).
Tests: tests/fluent-agents-filter.test.ts (DNF compilation incl. AND-over-OR
distribution, RevertTarget builders). Demo: fluent-demo durability-agent `peers`
lists the component's agents via getAgents + Filter.
… (Track C stage 1)
Remove the decorator/base authoring surface and its type machinery, leaving the
fluent (Standard Schema) surface as the package's only agent API. The shared
runtime guest (index.ts initialize/invoke/save/load) stays.
Deleted: baseAgent, decorators/, typescriptTypeRegistry, agentConfig, richTypes,
internal/{schema,mapping}/, typeInfoInternal, clientGeneration, http/mount, the 4
base param/method registries, try.ts + either.ts, and all base tests/fixtures/bench.
Rewired: index.ts drops base exports + the decorator validation-error gate (the
fluent surface validates eagerly during defineAgent); resolvedAgent.ts reduced to
the minimal ResolvedAgent interface the guest drives (FluentResolvedAgent already
implements it); internal/http/validation.ts slimmed to the two string validators
fluent uses; testSetup registers the vendor walkers via the barrel; test script
drops the decorator golem-typegen step.
Consequences of dropping base (intended): the `Config` class and richTypes
`Path`/`Duration`/`Quantity` go away; fluent's `Secret` is now the public `Secret`.
Green: 278 tests pass, rollup --failAfterWarnings clean, 0 lint errors.
…k C stage 2) Remove the TypeScriptFluent GuestLanguage variant and make TypeScript the fluent (Standard Schema) language: - GuestLanguage: drop TypeScriptFluent + its match arms; `ts`/`typescript` map to TypeScript; merge the `ts | ts-fluent` bridge/template/deploy/source-language arms. - check/requirements: TypeScript now uses the bundler-resolution tsconfig (no experimentalDecorators/emitDecoratorMetadata); removed the decorator tsconfig constant and the golem-ts-typegen optional dependency. - build.rs: SUPPORTED_LANGUAGES drops ts-fluent. - templates: replace the base decorator `ts` templates with the fluent ones (moved templates/ts-fluent -> templates/ts; renamed internal ts-fluent -> ts). The `ts` templates now ship only the `default` (fluent) variant; the json/snapshotting/human-in-the-loop variants are re-added in a later stage. Green: cargo check -p golem-cli clean; the two ts check tests pass.
…Track C stage 3) The decorator/base flow is gone, so the two typegen packages (which generated the .metadata for it) are fully dead — the fluent SDK derives types at runtime. Removed: the two package directories; golem-ts-sdk's devDeps on them; the root build script chain + the publish-golem-ts.yaml package loop; the GOLEM_TS_TYPEGEN_VERSION_OR_PATH template substitution (cli generator.rs); and the typegen build-order notes in sdks/ts README + AGENTS.md. Reconciled pnpm-lock.yaml. The edit/tests.rs JSON-merge fixtures now reference golem-ts-bridge. Green: cargo check -p golem-cli clean; golem-ts-sdk 278 tests pass.
…C stage 4) Add the missing fluent `ts` agent templates so `golem new` regains the variants the base SDK had, plus per-schema-library variants: - counter-valibot, counter-arktype: the default Counter authored with Valibot / ArkType (new GOLEM_TS_VALIBOT_VERSION / GOLEM_TS_ARKTYPE_VERSION placeholders; versions.rs VALIBOT/ARKTYPE consts + generator substitutions; valibot/arktype added as Optional deps in the ts build check). - json: TaskAgent ported to fluent (zod) — JSON object I/O + HTTP endpoints. - snapshotting: snapshot counter using the declarative `snapshotting` option. - human-in-the-loop: promise-based workflow (createPromise/awaitPromise/completePromise + clientFor RPC). Templates tsc-clean against the current fluent SDK; cargo check + the ts template↔check consistency tests pass.
…gen docs (Track C stage 6a) The base/decorator TS SDK is gone; rewrite every golem-skills/skills/ts/* SKILL.md to the fluent (Standard Schema) API — defineAgent/method/.implement, http.mount + per-method http verbs, the `s` schema markers, clientFor RPC, and the host helpers (saga/quota/ retry/durability/promises). Regenerated docs/src/content/next/how-to-guides MDX via `cargo make generate-docs-skills`. 30 skills changed; 9 were already authoring-API-agnostic (CLI/host/manifest guidance). Genuine fluent-vs-decorator deltas the rewrites surfaced (documented, for review): - `readOnly` is a bool only (no cache policy / TTL / per-principal caching); - no config-RPC-override (`getWithConfig`) client variant; - custom snapshot is a bare `Uint8Array` (no mimeType variant, no auto SQLite multipart); - no mount-level header→id binding (path vars only); - no cancelable scheduling in the fluent RPC client; - `Principal` is read via `this.getPrincipal()`, not a method parameter.
…age 6b) - templates/ts/common/AGENTS.md: replace the TODO stub with a full fluent-TypeScript development guide (golem-managed markers preserved) mirroring the Rust guide's shape — defineAgent/method/.implement, schemas + `s` markers, typed errors via s.result, HTTP via http.mount, config/secrets, clientFor RPC, snapshotting, durability, build/deploy. - .agents/skills/migrate-ts-base-to-fluent: new repo-dev skill mapping the decorator/base API to fluent (class→defineAgent, @endpoint/@readonly/@description→method options, Result→s.result, Config/Secret→config record, snapshot overrides→snapshotting), the tsconfig/import changes, the known fluent feature deltas, and the git-bundle archive convention.
Port the 6 decorator TS test-components to the fluent SDK (defineAgent/.implement, Standard Schema/zod), preserving agent-type + method + param names + returns so the integration tests' expectations hold. Removed the golem-ts-typegen devDep, added zod, and dropped decorator tsconfig options across all 6. 4 agents could NOT be faithfully ported — the fluent SDK lacks the capability (see tmp/tracks/README.md "FEATURE-GAP BLOCKERS"): PrincipalAgent (no Principal schema), TsCancelTester/TsCancelCallerAgent (no abortable RPC), RpcLocalConfigAgent (no config-on-RPC); TsReadonlyAgent degraded (readOnly is bool-only). They were removed/degraded with in-code notes. DEFERRED pending a decision (add fluent features vs skip tests): the wasm rebuild, build verification, and reconciling the 3 removed-agent integration tests (agent_http_principal_ts, rpc.rs cancel, agent_config/rpc).
…ck C)
Restore capabilities the base/decorator SDK had that fluent lacked (surfaced by the
test-component port). All 4 are SDK-side only (no agent_guest.wasm rebuild):
- F1 `s.principal()` marker — carry a Principal as a data value (WIT Principal
variant: oidc/agent/golem-user/anonymous). New marker + variant codec in
schema/markers.ts; non-bindable kind in httpTypes.ts. +5 round-trip tests.
- F2 abortable / cancelable RPC — `clientFor` methods gain `.abortable(signal, …)`
(cancels the remote future on abort) and `.scheduleCancelable(at, …)` →
CancellationToken (client.ts).
- F3 config-on-RPC — `clientFor(def)(id, phantomId?, config?)` encodes non-secret
config overrides into the target WasmRpc (rejects secret overrides), mirroring
base + effect (client.ts).
- F4 `readOnly` cache policies — `readOnly` widens from bool to
`boolean | { cache?: 'no-cache'|'until-write'|{ttlNanos}; usesPrincipal? }`, and
fixes the default regression: `readOnly: true` now uses `until-write` (base
default), not `no-cache` (method.ts, runtime.ts; test updated).
Effect-parity audit: tmp/fluent-feature-gaps-and-effect-parity.md — effect-golem
already has F2+F3 (both lines); F1+F4 must still be backported to effect.
Green: build clean, 283 tests pass, 0 lint errors.
…tures (Track C stage 5) Re-port the agents removed during the initial port, now that fluent has the features: - TsCancelTester/TsCancelCallerAgent (agent-rpc): abortable RPC (`.abortable(signal, …)`). - RpcLocalConfigAgent (agent-sdk-ts/config): config-on-RPC (`clientFor(id, undefined, overrides)`). - TsReadonlyAgent (agent-sdk-ts/readonly): readOnly cache policies (until-write / ttl / no-cache / usesPrincipal). - PrincipalAgent (agent-sdk-ts/http): `s.principal()` for Principal return values. PrincipalAgent uses `phantomAgent: true` for the per-request caller principal, because fluent's HTTP Principal auto-injection as a method INPUT parameter is not wired (a 5th gap — see tmp/fluent-feature-gaps-and-effect-parity.md). Passes agent_http_principal_ts.rs (asserts only HTTP responses).
The unit tests only exercised `compileSchema(s.principal())`; add a test that runs `defineAgent` with `s.principal()` nested in a method return (the full GraphEncoder assembly path) so the marker is covered end-to-end. Also correct the feature-gap audit: the 4 base-parity features (s.principal, abortable RPC, config-on-RPC, readOnly) DO require rebuilding agent_guest.wasm — the ts component template externalizes @golemcloud/golem-ts-sdk to the embedded runtime, so new SDK runtime code only reaches components after `pnpm run build-agent-template`.
…rity)
Properly wire the caller Principal as an auto-injected method/constructor input,
matching the base/decorator SDK exactly (no phantom-agent workaround). A bare
`s.principal()` parameter carries WIT `field-source.auto-injected(principal)`: the
host supplies the per-call caller principal, so it consumes NO wire field and is
not bound from HTTP/RPC callers.
- schema/codec.ts + schema/markers.ts: FluentCodec gains `autoInjected?: 'principal'`,
set by the principal marker (a nested/return principal stays user-supplied data).
- runtime.ts: `encodeInput` emits `auto-injected(principal)` for such params;
`invoke` + `initiate` decode with a cursor — injected params are filled from the
separate `principal` arg and consume no wire field (mirrors base `decodeInputRecord`).
- httpTypes.ts: `AutoInjectedKeys` excludes principal params from the bodyless
GET/HEAD "every param must be bound" rule (in the real `ValidateEndpointsTuple`).
- client.ts: RPC caller input `CallerInput<>` omits the principal param and the
encoder skips it, so user-supplied fields stay aligned with the callee's decode.
- defineAgent.ts: expose `config` on AgentDefinition (fixes a latent F3 type gap).
- test-components PrincipalAgent: declare `input: { caller: s.principal() }`, return
it; drop the phantomAgent hack. +SDK test asserting the auto-injected source.
All 4 TS test-components build clean; 285 SDK tests pass; type-level verified
(principal-on-GET valid, unbound param still rejected).
…g-on-RPC) defineAgent passed config to registerAgentType metadata but omitted it from the returned def object, so clientFor's compileConfig(def.config) saw undefined and encoded NO config overrides — config-on-RPC silently sent nothing (the callee failed with 'Config <x> was not provided'). Set config on the returned def. Surfaced by the agent_config/rpc.rs integration tests.
It is a working scratchpad, not a repo deliverable — keep it on disk (untracked).
|
📖 Docs preview: https://docs-5eqzken7d-golem-cloud.vercel.app Built from commit |
…e semantics
An optional object config group (`z.object({...}).optional()`) was compiled to a
whole `option<record>` leaf, so its child paths were never declared to the host
("Agent type does not declare local config <group>.<child>") and the group read
back as null instead of an object.
Fix (mirrors the base SDK): descend optional groups into per-leaf declarations and
decide group presence by its REQUIRED children.
- The vendor walkers (zod/valibot/arktype/effect) expose `fields` + a new
`FluentCodec.optionalGroup` flag on an optional OBJECT so the config surface can
descend it.
- config.ts builds a ConfigGroupNode/ConfigLeafNode presence tree: a required
(non-optional) leaf under an optional ancestor is OPTION-LIFTED (declared
`option<inner>`) so an unset read returns none instead of trapping the guest
(host `get-config-value` traps on a missing non-option leaf — uncatchable in JS).
At runtime a group materializes to `undefined` iff any required child is unset;
an all-optional group is always present (`{}`), a missing required nested leaf
prunes the whole ancestor chain.
- runtime.ts carries the presence tree; declarations derive from it.
+5 unit tests (descend + option-lifting + requiredKeys). 290 SDK tests pass.
… fixes The 4 base-parity features (+ principal input auto-injection) are now implemented and verified, so the skills/migration guide must stop describing them as missing: - migrate-ts-base-to-fluent: rewrote the "known feature deltas" section — readOnly cache policies, config-on-RPC (getWithConfig), abortable/scheduleCancelable RPC, and Principal-as-data + auto-injected-input param are now documented as SUPPORTED (only bare-Uint8Array snapshot + no mount-level header binding remain as deltas); updated the @readonly and injected-Principal mapping rows. - golem-mark-read-only-ts: readOnly now takes `{ cache: no-cache|until-write|{ttlNanos}, usesPrincipal }`, not boolean-only. - golem-add-config-ts: config-on-RPC overrides via clientFor(id, phantomId?, overrides). - golem-add-http-auth-ts: Principal can be an auto-injected `s.principal()` parameter. - golem-schedule-future-call-ts: `.scheduleCancelable(at,input) → CancellationToken` (+ `.abortable`); dropped the now-obsolete "no cancelable variant" workaround. - golem-multi-instance-agent-ts: documented the config-override 3rd arg + the .trigger/.schedule/.scheduleCancelable/.abortable method variants. Regenerated docs/.../how-to-guides/ts/*.mdx from the skills (drift-check clean).
…files Formatting-only: rustfmt reflow of the TS-language check/gen_bridge arms and prettier line-wrapping in vitest.config.ts. No logic changes.
…rack C)
Track C dropped the base/decorator SDK; the golem-cli integration tests still used
`BaseAgent`/`@agent` + the removed `@golemcloud/golem-ts-typegen`, so their inline TS
fixtures failed to compile (it-cli CI). Migrate them to the fluent `defineAgent` API.
- app.rs / agents.rs / directory_source_ifs.rs: convert every inline decorator agent
to `defineAgent(...).implement(...)` (zod Standard Schema), preserving agent-type
names, method/param names, and behavior (config/secret via config record + s.secret,
RPC via clientFor, IFS, REPL introspection).
- Drop the golem-ts-typegen devDependency + experimentalDecorators tsconfig injection
(fluent has no typegen step); keep `zod` in the deps-wipe (fluent agents import it).
- test-data/ts-code-first-snippets/{main,model,naming_extremes}.ts → fluent.
- templates/ts/{default,counter-arktype,counter-valibot}: rename the example agent
`Counter` → `CounterAgent`/`CounterArktype`/`CounterValibot` (unique agent-type names
across components; matches the tests' expected `CounterAgent`).
Verified: `build_check` passes (scaffold + fluent build + check-requirements). 11 of the
12 it-cli TS tests are fixture-only fixes; test_ts_code_first is tracked separately
(needs SDK recursive-schema `z.lazy` + `z.null` support + goldenfile regen).
…ias)
The fluent schema walkers inlined every schema, so a self- or mutually-recursive
type (e.g. `Tree = z.lazy(() => z.object({ label, children: z.array(Tree) }))`)
infinite-looped in `compileSchema`. Add cycle detection so recursive types compile
to the flat WIT carrier (schema-graph `defs` + `ref` nodes) the encoder already
supports — restoring base-parity for recursive types.
- recursion.ts: a per-compile `RecursionRegistry` keyed on schema-node identity;
reserves a type-id, hands out a late-binding `ref` codec on re-entry (a real
cycle), and promotes to a named `ref` def ONLY if the ref was actually taken —
non-recursive schemas stay inline byte-for-byte (no behavioral change).
- adapter.ts: thread the registry through `compileSchema`/`recurse`.
- zod (ZodLazy.getter) / valibot (lazy.getter) / effect (Suspend.f) / arktype
(alias.resolution): recognize the deferred node and recurse through the registry.
- tests/fluent-recursive.test.ts: self- + mutually-recursive round-trips for all
four vendors, WIT graph->wire->graph encode, and non-recursive inline regression
guards. 299 tests pass.
…_code_first) test_ts_code_first_with_rpc_and_all_types still fails, but the cause is now a golem-cli deploy-time Rust stack overflow on recursive agent-type schemas, not the migrated fixture. Add a TODO documenting the reproduction + localization (command_handler/component/mod.rs metadata step; a ref-walk lacking cycle detection) and the goldenfile-regen follow-up, to aid the fix. Keep the test active (not ignored) so the regression stays visible.
vigoo
left a comment
There was a problem hiding this comment.
A few gaps around compile-time safety and client ergonomics, left as inline comments.
| * skip these gates and are validated only by the runtime checks in | ||
| * `runtime.ts`. | ||
| */ | ||
| http?: MountSpecCovering<Id, MV, WV> & WebhookVarsValid<Id, WV>; |
There was a problem hiding this comment.
http stays optional even when one of the methods declares HTTP endpoints, so forgetting the mount only fails at runtime (the 'declares HTTP endpoint(s) but the agent has no HTTP mount' throw in runtime.ts). Since MethodSpec already carries its endpoints, we could track a "has HTTP" flag at the type level (e.g. a boolean phantom on MethodSpec, aggregated over Methods) and make http a required field of AgentSpec whenever any method declares endpoints. That turns a deploy-time error into a compile error, and we already do similar type-level work here with MountSpecCovering / WebhookVarsValid.
| /** Wrappers that pass through their inner schema unchanged. */ | ||
| const TRANSPARENT = new Set(['default', 'readonly', 'catch', 'effects', 'nonoptional', 'pipe']); | ||
| /** Wrappers that make the value optional. */ | ||
| const OPTIONAL = new Set(['optional', 'nullable']); |
There was a problem hiding this comment.
optional and nullable are both collapsed into the same WIT option codec, and fromValue decodes the none case to undefined unconditionally. That breaks round-tripping for nullable fields: a field declared z.number().nullable() infers as number | null, but after a round trip the receiver gets undefined — a value that isn't even assignable to the schema's own inferred type. Same issue in the valibot walker (nullable / nullish / undefinedable all in one OPTIONAL set).
Suggest recording which wrapper produced the option and decoding none accordingly: undefined for optional, null for nullable, and picking one (or undefined) for nullish. The wire format doesn't change; only the decode side needs the distinction.
| */ | ||
| export type SnapshottingSpec = | ||
| | SnapshotPolicy | ||
| | { policy?: SnapshotPolicy; state: StandardSchemaV1 }; |
There was a problem hiding this comment.
In { policy, state }, state is an untyped StandardSchemaV1, so nothing connects it to the State object that init actually returns. A typo'd field name, or drift after a refactor of init, compiles fine and only surfaces when validateSnapshotState rejects the instance at the first snapshot save — on a deployed agent.
Since State is only known at implement(...) time, one way to get compile-time checking: thread the snapshot schema type through AgentSpec into AgentImplementation and constrain State against the schema's output type (e.g. State extends StandardSchemaV1.InferOutput<StateSchema>), so the schema-declared fields must exist on the state with matching types.
| * c1.increment.trigger({ by: 1 }); // fire-and-forget | ||
| * ``` | ||
| */ | ||
| export function clientFor<Id extends IdRecord, Methods extends MethodsRecord>( |
There was a problem hiding this comment.
A few ergonomics gaps on the client surface:
- Fresh phantom clients.
clientFor(def)(id, phantomId?)supports existing phantom ids, but there's no helper for "create a new phantom instance and give me its id" — callers must generate aUuidthemselves and thread it through. Consider something likeclient.newPhantom(id)returning the client with the generatedphantomIdexposed on it (mirroring howfork()returnsForkDetails). - Cancellation on awaited calls. The plain call
method(input)can't be cancelled; you must switch toabortable(signal, input), which also reorders the arguments. An optional trailing options argument on the main call shape (method(input, { signal })) would keep one call shape and make cancellation compositional with existingAbortSignal-based code. schedulevsscheduleCancelable.schedule()returnsvoidwhilescheduleCancelable()returns aCancellationToken. Unless the non-cancelable host path is meaningfully cheaper, consider havingschedulealways return the token — one method, and cancellation doesn't require deciding upfront.
| throw createCustomError(validationError.message); | ||
| } | ||
|
|
||
| // The fluent surface validates eagerly (throws during `defineAgent(...)` at |
There was a problem hiding this comment.
defineAgent(...) throwing at module load means a single invalid agent definition takes down the whole component at instantiation: the guest never gets far enough for discover-agent-types to run, so the CLI/registry see a generic wasm trap instead of a structured, per-agent diagnostic. This also means one bad agent hides all the valid ones in the same component.
Consider capturing definition-validation failures at defineAgent/implement time and re-emitting them as typed AgentErrors from discoverAgentTypes (and from initialize for the affected agent type), so tooling can render "agent X is invalid because Y" while other agents in the component keep working. Eager throwing could stay as-is under a dev/test flag where fail-fast is preferable.
Uh oh!
There was an error while loading. Please reload this page.