Add multi-agent chat example with browser acceptance tests#4
Open
taras wants to merge 135 commits intocowboyd:mainfrom
Open
Add multi-agent chat example with browser acceptance tests#4taras wants to merge 135 commits intocowboyd:mainfrom
taras wants to merge 135 commits intocowboyd:mainfrom
Conversation
Add the five core specification documents: - tisyn-specification-1.0.md: System specification (v1.0.0) covering IR grammar, evaluation model, environment, structural/external operations, concurrency, resolve, validation, runtime, wire protocol, determinism, error semantics, and agent abstraction. - tisyn-architecture.md: Architecture and design rationale covering the pipeline, execution flow, IR design, kernel model, effects/ journaling, deterministic replay, concurrency, agent model, error/ cancellation flow, transport, compiler, and comparison to existing systems. - tisyn-kernel-specification.md: Kernel specification (v1.0.0) detailing evaluation rules, environment model, resolve function, eval node execution, structural operations, quote semantics, concurrency model, error handling, journaling, replay semantics, determinism guarantees, and validation. - tisyn-compiler-specification-1.1.0.md: Compiler specification (v1.1.0) covering the compilation pipeline, yield* desugaring, sequential statement compilation, control flow transformations, expression compilation, concurrency compilation, function semantics, and unsupported constructs. - tisyn-agent-specification-1.1.0.md: Agent implementation specification (v1.1.0) covering agent role, operation registration, execution contract, idempotency, cancellation, concurrency, error handling, serialization, reconnect/duplicates, and progress reporting.
Set up the monorepo structure: - pnpm workspace with 6 packages (shared, kernel, durable-streams, runtime, agent, conformance) - Root tsconfig.json with project references following the effectionx pattern (composite, NodeNext, ES2022) - oxlint for linting, oxfmt for formatting - vitest for testing in conformance package - Effection ^4 as peer dependency - Removed Deno configuration (deno.json, mod.ts)
…ncoding Implement @tisyn/shared with the core protocol types: - ir.ts: Five IR node types (Eval, Quote, Ref, Fn, Literal) with type guards and classifyNode() for validation - values.ts: Json and Val types per System Spec §2.2 - events.ts: YieldEvent, CloseEvent, EffectDescriptor, EventResult per Conformance Suite §4.5 - errors.ts: All 9 error types from Conformance Suite §4.1 (MalformedIR through DivergenceError) - canonical.ts: Canonical JSON encoding per Conformance Suite §4.3 (lexicographic keys, shortest numbers, no whitespace) - effect-id.ts: parseEffectId() per Kernel Spec §4.6
Implement @tisyn/kernel with the full evaluation model: - environment.ts: Linked-list immutable frames with lookup, extend - classify.ts: STRUCTURAL/EXTERNAL classification of Eval IDs - eval.ts: Generator-based evaluator implementing all 5 rules (Literal, Ref, Quote, Fn, Eval) and all 18 structural operations (let, seq, if, while, call, get, arithmetic, comparison, equality, short-circuit, unary, construct, array, concat, throw) - resolve.ts: Recursive resolution with opaque value rule — values from lookup/eval are terminal, never recursed into - unquote.ts: Quote stripping for structural operation data - validate.ts: Two-phase validation (structural grammar + semantic single-Quote rule with positions table) The kernel is a pure generator that yields EffectDescriptors. It knows nothing about agents, journals, or transport.
…ation Implement @tisyn/durable-streams: - stream.ts: DurableStream interface (readAll, append) and InMemoryStream with clone-on-write safety, appendCount tracking, and injectFailure for persist-before-resume testing - replay-index.ts: Per-coroutine cursored access to stored Yield events with peekYield/consumeYield and Close event lookup Mirrors the @effectionx/durable-streams API pattern. Operations are Effection generators for compatibility with the runtime.
Implement @tisyn/agent:
- AgentRegistry with register() and dispatch() — parses dotted
effect IDs and routes to registered agent handlers
Implement @tisyn/runtime:
- execute() Operation that drives the kernel generator with:
- IR validation before evaluation (MalformedIR → no journal)
- ReplayIndex-first dispatch (match by type+name only)
- Divergence detection (D1: description mismatch, D2: continue
past close) per Conformance Suite §10.4
- Live dispatch to agents via AgentRegistry
- Persist-before-resume invariant (Yield written before kernel
resumes)
- Close(ok) on completion, Close(err) on error
- Effect errors re-raised into kernel via generator.throw()
Full monorepo builds cleanly with tsc --build.
Implement @tisyn/conformance: - harness.ts: Test runner supporting evaluation, effect, replay, negative_validation, and negative_runtime fixture types with canonical comparison, '<any>' sentinel handling, and mock agents - fixtures.ts: All 11 Core conformance fixtures from the spec: KERN-001 (literal), KERN-020 (let), KERN-034 (call-site resolution), KERN-080 (sequential effects), KERN-071 (opaque value rule), REPLAY-010 (partial replay), REPLAY-020 (divergence detection), NEG-020 (malformed IR), NEG-001 (unbound variable), DET-005 (float determinism), KERN-014 (short-circuit) - crash-replay.test.ts: End-to-end crash/replay scenario verifying stored effects replay without re-executing and live dispatch resumes Fix replay journal tracking: stored events are only included in the returned journal when successfully consumed (not on divergence). All 12 tests pass.
- Remove unused imports (isEvalNode, isRefNode, isFnNode from validate.ts; ExecuteOptions from harness.ts; Val from crash test) - Remove unused variable (yieldIndex, obj) - Prefix unused parameters with underscore - Add .oxlintrc.json disabling require-yield (intentional synchronous generators for Effection Operation interface) - Apply oxfmt formatting across all source files Build: clean. Lint: 0 warnings, 0 errors. Tests: 12/12 pass.
…er, errors Create packages/compiler with foundational modules: - ir-builders.ts: Pure constructors for all Tisyn IR nodes — structural ops (Let, If, While, Call, Get, etc.), arithmetic, comparison, equality, logical, unary, Construct, Array, Concat, Throw, ExternalEval, AllEval, RaceEval. Each wraps data in Quote for structural ops, leaves data unquoted for external ops. - agent-id.ts: PascalCase → kebab-case conversion per Compiler Spec §4.2 (OrderService → 'order-service') - counter.ts: Monotonic counter for deterministic synthetic names (__discard_0, __loop_0, __sub_0, __all_0) - errors.ts: CompileError with source location and all error codes E001–E030 from Compiler Spec §11
Implement @tisyn/compiler per Compiler Specification 1.1.0:
Pipeline (4 phases):
- Parse: ts.createSourceFile → extract generator function declarations
- Discover: Classify yield* sites, detect unsupported constructs
- Transform: Early return rewrite, while-with-return → recursive Fn+Call
- Emit: AST → Tisyn IR with kernel validation (default on)
Expression compilation (§7):
- Literals, identifiers (→ Ref), property access (→ Get)
- All arithmetic/comparison/equality/logical operators
- Object literals (→ Construct), arrays (→ Array)
- Template literals (→ Concat), arrow functions (→ Fn)
- Ternary expressions (→ If), throw (→ Throw)
Statement compilation (§5):
- Block-to-Let transformation for sequential const declarations
- Early return: code after if-with-return moves into else branch
- While (Case A, no return) → While IR node
- While (Case B, with return) → recursive Fn + Call via call-site
resolution, matching Compiler Spec §6.2 exactly
- Discarded effects → Let with synthetic __discard_N names
Effect compilation (§4):
- Agent effects: yield* X().method(args) → ExternalEval with
kebab-case agent ID (OrderService → 'order-service')
- Concurrency: yield* all/race([...]) → AllEval/RaceEval with
quoted data and unwrapped arrow function bodies
- Built-ins: yield* sleep(ms) → ExternalEval('sleep', [...])
- Sub-workflows: yield* fn(args) → Call (Strategy B)
Error detection (§11):
- E001 (let), E002 (var), E003 (reassignment), E004 (mutation),
E005 (computed access), E006 (Math.random), E007 (Date.now),
E008 (Map/Set), E009 (async), E010 (yield* in expr), E014 (eval),
E017 (yield no *), E023 (non-Error throw), E024 (arrow block body),
E028 (__ prefix), E029 (delete)
Tests (39 passing):
- All 3 spec end-to-end examples (§12.1, §12.2, §12.3)
- Expression compilation for every operator type
- Control flow: if/else, while, early return, throw
- Effects: agent calls, sleep, discarded effects
- Error detection: E001, E003, E005, E017, E023, E024, E028
- Validation: default-on kernel validate() on output
Moved all spec files from repo root to specs/: - tisyn-specification-1.0.md - tisyn-kernel-specification.md - tisyn-agent-specification-1.1.0.md - tisyn-compiler-specification-1.1.0.md - tisyn-architecture.md - tisyn-pingpong-specification.md Removed duplicate demos/ping-pong/spec.md (canonical copy now in specs/). Session-ID: ses_o4mni6hgrj
Session-ID: ses_o4mni6hgrj
Introduce the foundational @tisyn/ir package per the Authoring Layer Specification v0.3.0. This zero-dependency package provides phantom-typed Expr<T> constructors, grammar types, data shapes, classification, walk/fold/transform traversal, print/decompile output, and collect utilities. Existing packages (@tisyn/shared, @tisyn/compiler) now depend on and re-export from @tisyn/ir.
Add GitHub Actions "Verify" workflow that runs lint, format check, type check, and tests on push/PR to main. Apply oxfmt auto-formatting to bring all files into compliance.
The TypeScript build requires Node type declarations for node:fs, node:path, etc. These were resolved transitively locally but not in CI.
6 replay tests covering hit, miss, divergence (wrong type, wrong name, continue past close), and data-ignored replay. 1 journal test verifying persist-before-resume ordering.
Apply oxfmt formatting to runtime test files and update pnpm-lock.yaml to include vitest@3.2.4 for the runtime package.
Upgrade vitest from v3 to v4 across all packages. Replace manual run() wrapping with @effectionx/bdd's native generator support in test files that use effection (runtime and conformance packages).
Switch from @effectionx/bdd (node:test runner) to @effectionx/vitest so Effection-aware tests run natively through vitest. This eliminates the separate `node --test` step for crash-replay and runtime tests. - Update imports from "@effectionx/bdd/node" to "@effectionx/vitest" - Simplify test scripts to just "vitest run" - Remove crash-replay.test.ts exclusion from conformance vitest config - Add vitest config to runtime package to exclude dist/
Add compound effect interception in the runtime to orchestrate `all` and `race` expressions with Effection structured concurrency. The kernel now attaches parent env to compound descriptors so child kernels resolve refs in the correct scope. The runtime strips __env at the orchestration boundary, spawns child tasks with deterministic coroutineIds, and handles cancellation journaling via ensure() with a closed-flag gate. Includes substrate conformance, task orchestration, recovery/replay, and cancellation tests (20 new tests), plus the compound concurrency spec.
1. Persist Close(cancelled) to durable stream — ensure() now uses a generator to yield* stream.append(), not just in-memory journal push. 2. Make orchestrateAll fail-fast — reject immediately on first child error instead of waiting for all N children to complete. 3. Add pre-close check for cancelled children during replay — skip kernel entirely for children with Close(cancelled) in replay index. Fix race replay test to use closeCancelled for loser. 4. Propagate DivergenceError fatally — re-throw after persisting Close(err) to stream. Update conformance harness to handle thrown DivergenceError in replay fixtures. 5. Add V1/V2 substrate conformance tests — V1 verifies all children close on scope exit, V2 verifies child closes precede post-compound effects in journal.
- resolve.ts: use sorted Object.keys() instead of unsorted Object.entries() - guards.ts: reject empty string eval id in classifyNode - guards.ts: reject extra fields on all tagged node types (eval/quote/ref/fn) - Add NEG-021 and NEG-022 conformance fixtures
…1/E-1/L1 - Revert extra-field rejection in classifyNode (spec gap, not a bug) - D-4: execute() catches DivergenceError and returns error result - D-2: refactor __env to __tisyn_env/__tisyn_inner wrapper struct - D-3: harness uses canonical byte comparison for journal events - B-1: document race error selection spec inconsistency - E-1: document canonical string encoding status - L1: add KERN-050 (construct field order) and DET-002 (resolve key order) fixtures
Introduce agent(), operation(), implementAgent(), and Dispatch middleware via effection/experimental createApi. Remove AgentRegistry entirely. - agent(id, ops) declares a shared contract with host-side call methods - implementAgent(decl, handlers) binds implementations as dispatch middleware - Dispatch uses effection scope context instead of explicit registry passing - Runtime execute() no longer accepts agents option; dispatch is contextual - All runtime/conformance tests migrated from AgentRegistry to Dispatch.around - Added @tisyn/agent test suite covering invocation, dispatch, and error cases
The compiler/runtime treat external effect data as a single payload expression, but the public Eval() constructor accepted data as an array. This caused print() to wrap non-array data in [...], changing program semantics when switching between JSON and printed IR forms. - Change Eval() signature from data: Expr<unknown>[] to data: Expr<unknown> - Remove array wrapping in print() for external eval nodes - Simplify decompileExternalCall() to always render single payload - Update all test call sites to use single-payload convention
Implements the multi-agent-chat spec: a host-controlled chat loop across two transport-backed agent boundaries (WebSocket browser agent, Worker LLM agent) with a compiled workflow driving the orchestration. - Compilable workflow using Browser, LLM, and State agent contracts - Worker LLM agent with echo stub via workerTransport - Browser agent via serverWebSocketTransport + installRemoteAgent - Local State agent for cross-turn history accumulation - Vanilla HTML browser client implementing JSON-RPC agent protocol - Phased test suite: local agents → worker → websocket → end-to-end
Add *.generated.ts to .gitignore and remove the committed workflow.generated.ts — it should be produced by build:workflow.
The generated workflow file should be committed so the example works from a clean checkout. Build integration fixes still needed.
- Replace Call(chat as never) with Call(chat) in all 4 files now that Call() accepts TisynFn<A, R> directly - Add extractMessage() runtime narrowing helper in phase4-e2e test instead of raw cast on protocol args payload
Case A (no-return while loops) was compiling each body statement as an
independent expression, discarding variable names from const declarations.
Now emits a single Let/Seq chain via emitStatementList, preserving
bindings within each iteration.
Removes the if (false) { return; } workaround from the chat demo that
was forcing Case B to get proper bindings.
Define Workflow<T> = Generator<unknown, T, unknown> so authored workflow source can import the type and type-check in editors. The compiler continues to recognize Workflow<T> by identifier name as before. Removes workflow.ts from tsconfig exclude — it now type-checks cleanly.
…ports - Rename LLM() to Llm() in workflow source for consistent PascalCase - Simplify browser-transport and host wiring - Move @effectionx/worker and @effectionx/node to dependencies - Add reviewer topics for Case A bindings and spec alignment - Add types: ["node"] to tsconfig, remove build-workflow exclusion - Regenerate workflow.generated.ts
- Fix WebSocket message handling and channel close in browser transport - Use once() for connection and close events instead of on() - Add dev:browser script to serve browser client - Update generated workflow: l-l-m.sample → llm.sample
…col stack Convert the multi-agent-chat browser UI from a single static HTML file with inline JS to a React app backed by the framework's own protocol stack (implementAgent, createProtocolServer, @effectionx/websocket). The browser now uses the same Effection-based agent pattern as the LLM worker: implementAgent(Browser(), handlers) wired through createProtocolServer over a WebSocket transport. A createQueue bridge connects React's submit handler to the Effection waitForUser generator. - Add React/Vite/websocket devDependencies (example-local only) - Add tsconfig.browser.json for JSX/DOM support - Add protocol-server sub-path export to @tisyn/transport to avoid bundling Node-only transports (stdio → ctrlc-windows) in the browser - Add vitest.config.ts to prevent vitest inheriting Vite's browser root
- Add --journal <path> CLI flag for file-backed durable journaling (NDJSON) - Reconstruct chat history from journal events on restart - Filter CloseEvents on replay so terminated journals resume correctly - Add structured logging with [scope] tags across host, browser transport, LLM worker, and state agent - Optional DEBUG_CHAT=1 for verbose protocol-level logging
- Add hydrateTranscript and setReadOnly operations to Browser agent - Hydrate browser transcript from host history on reconnect - Show read-only state when workflow has already completed/failed - Fix CTRL+C hang by providing connection resources via stream with explicit spawn/halt lifecycle management per iteration - Use createSignal to map raw WebSocket connections to Operation<WebSocket> resources that clean up when their scope exits - Add readOnlyRef to browser useChat to suppress false "Host shut down" - Improve file journal error messages for malformed NDJSON - Replace verbose JSON IR with builder functions in workflow.generated.ts
- Change LLM agent ID from "l-l-m" to "llm" to match workflow.generated.ts - Replace import.meta.resolve() with new URL(_, import.meta.url).href for Vitest module runner compatibility
- Add BrowserSessionManager with identity-safe socket attach/detach, reconnect-safe waitForUser via withResolvers, and single-owner model - Install Browser as local agent backed by session manager instead of installRemoteAgent with serverWebSocketTransport - Replace JSON-RPC browser protocol with simple typed messages - Simplify useChat.ts to raw WebSocket client with localStorage session ID - Remove serverWebSocketTransport and JSON-RPC helpers from browser-transport - Rewrite phase3 tests for session manager reconnect semantics - Rewrite phase4 e2e test for new message protocol - Fix host staying alive after workflow start with never-resolving yield
… agents - Rename phase1-4 test files to behavior-based names (workflow, worker-transport, browser-session, e2e) - Extract repeated agent declarations into test/helpers/agents.ts - Remove phase language from describe titles and JSDoc comments
Implement the tisyn-browser-test-orchestration spec with 4 Playwright- based acceptance tests driven as Tisyn workflows: - basicSendReceive - transcriptRestoresAfterReload - hostRestartPreservesState - secondBrowserIsReadOnly Prerequisites: add ARIA roles/labels for accessible selectors, make host port configurable (--port flag), derive useChat WS URL from window.location, and add a runner-owned reverse proxy for static files + WS forwarding. Test infrastructure uses @effectionx/process daemon for host lifecycle, @effectionx/converge for readiness polling, and per-test isolation (fresh process, proxy, browser context, and temp journal).
Remove tsx from the browser-test-critical path by using node --experimental-strip-types for build-workflow.ts and build-test-workflows.ts. Host runtime stays on emitted JS (node dist/host.js via tsc emit). Also includes: - tsc emit config (tsconfig.build.json) for host runtime - Host startup diagnostics (stderr/stdout capture on failure) - Local-listen preflight check in browser acceptance suite - Workflow file imports aligned with compiler requirements
The compiler already generates identical agent declarations in workflow.generated.ts and workflows.generated.ts. Remove the redundant hand-written agents.ts files and update all consumers to import factory functions from the generated modules.
- runScenario() accepts TisynFn<[], unknown> instead of an Effection
function, removing the Call(workflow as any) cast
- Handler factories derive types from generated TestBrowser()/TestHost()
via ImplementationHandlers, eliminating manual { input: ... } shapes
Use resource + withResolvers for the WebSocket readiness probe, ensuring ws.close() runs on cancellation, success, and error. Removes manual setTimeout and new Promise wrapper.
- Build scripts: readFileSync/writeFileSync/readdirSync → fs/promises with top-level await - FileJournalStream: async readFile/writeFile with flush:true for durability (fsync before resolve) - Proxy: async readFile in HTTP handler with try/catch for ENOENT - Scenario: mkdtempSync/rmSync → async via call() - Tests: unlinkSync → async unlink, readFileSync → async readFile
Replace fine-grained host-visible browser operations (fill, click,
expect*) with a single Browser().execute({ workflow: <ir-payload> })
that sends compiled DOM workflow IR to run inside the browser page
via a local Dom agent and Tisyn executor.
- Two-pass build: dom workflows (JSON IR) then host workflows
- In-page executor bundled as IIFE via Vite, injected per page load
- Dom agent uses @testing-library/dom for semantic queries with retry
- Host workflows reference dom IR as free variables (Ref → env lookup)
- Fix ECONNRESET in proxy during host restart tests
- Delete manual createDomAgent(); import generated Dom() instead - Cast input.workflow to unknown at page.evaluate boundary to avoid excessively deep type instantiation from IrInput union
Add a Vite plugin that proxies WebSocket upgrades to the host on port 3000, restoring the dual-process demo workflow (host + Vite dev server). Skip Vite HMR connections via sec-websocket-protocol header check.
Rename the app workflow's Browser agent to App to better reflect its role. Test-side Browser agent (Playwright sessions) unchanged. Also update reconstructHistory and journal test fixtures to match the new "app" agent ID.
…t ops Consolidate browser session management into the Chat agent. Replace App().waitForUser/showAssistantMessage/hydrateTranscript and State agent with Chat().elicit/renderTranscript/setReadOnly. History is now managed workflow-locally rather than via a separate State agent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
examples/multi-agent-chat) using Tisyn's compiled workflow, durable journaling, and WebSocket browser transport@testing-library/domDom agentBrowser().execute({ workflow: <ir-payload> })pattern — DOM interactions run inside the page via a local Dom agent, crossing the host/browser boundary once per scenario fragmentPackage changes
@tisyn/agent: exportWorkflow<T>public type@tisyn/ir: exportIrInputandTypedIrNode; fixCall()signature@tisyn/compiler: fix codegen for composite declarations,whileloop Case A bindings, IRprint()output@tisyn/runtime: alignexecute()with single-payloadEval()canonical form@tisyn/transport: add./protocol-serversub-path exportTest plan
pnpm run test— 21 unit tests passpnpm run test:browser— 4 browser acceptance tests pass (basic send/receive, transcript restore, host restart, second browser read-only)