Skip to content

TS SDK redesign#3680

Open
noise64 wants to merge 43 commits into
mainfrom
new-ts-sdk-init
Open

TS SDK redesign#3680
noise64 wants to merge 43 commits into
mainfrom
new-ts-sdk-init

Conversation

@noise64

@noise64 noise64 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

noise64 added 12 commits June 29, 2026 20:57
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).
@netlify

netlify Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploy Preview for golemcloud canceled.

Name Link
🔨 Latest commit 451ce80
🔍 Latest deploy log https://app.netlify.com/projects/golemcloud/deploys/6a4cefe90db5010008e6a8bc

noise64 added 17 commits July 1, 2026 17:40
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).
noise64 added 6 commits July 7, 2026 06:46
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).
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📖 Docs preview: https://docs-5eqzken7d-golem-cloud.vercel.app

Built from commit 451ce807217360a123f98602d832961fc95e43b6 by docs.yaml.

noise64 added 3 commits July 7, 2026 10:19
…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).
@noise64 noise64 self-assigned this Jul 7, 2026
@noise64 noise64 added this to the Golem 1.6 milestone Jul 7, 2026
@noise64 noise64 marked this pull request as ready for review July 7, 2026 08:48
@noise64 noise64 requested a review from a team July 7, 2026 08:48
noise64 added 5 commits July 7, 2026 10:49
…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 vigoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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']);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few ergonomics gaps on the client surface:

  1. 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 a Uuid themselves and thread it through. Consider something like client.newPhantom(id) returning the client with the generated phantomId exposed on it (mirroring how fork() returns ForkDetails).
  2. Cancellation on awaited calls. The plain call method(input) can't be cancelled; you must switch to abortable(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 existing AbortSignal-based code.
  3. schedule vs scheduleCancelable. schedule() returns void while scheduleCancelable() returns a CancellationToken. Unless the non-cancelable host path is meaningfully cheaper, consider having schedule always 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

Fluent TS SDK: Docs Fluent TS SDK: Switch to fluent API Fluent TS SDK: Feature parity, finalized naming Fluent TS SDK: Basic agent features

2 participants