@agentick/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +270 -64
  2. package/dist/.tsbuildinfo.build +1 -1
  3. package/dist/agentick-instance.d.ts.map +1 -1
  4. package/dist/agentick-instance.js +125 -119
  5. package/dist/agentick-instance.js.map +1 -1
  6. package/dist/app/session-store.d.ts +1 -1
  7. package/dist/app/session-store.js +1 -1
  8. package/dist/app/session.d.ts +26 -17
  9. package/dist/app/session.d.ts.map +1 -1
  10. package/dist/app/session.js +222 -204
  11. package/dist/app/session.js.map +1 -1
  12. package/dist/app/types.d.ts +230 -149
  13. package/dist/app/types.d.ts.map +1 -1
  14. package/dist/com/object-model.d.ts +7 -4
  15. package/dist/com/object-model.d.ts.map +1 -1
  16. package/dist/com/object-model.js +13 -4
  17. package/dist/com/object-model.js.map +1 -1
  18. package/dist/compiler/collector.d.ts +1 -1
  19. package/dist/compiler/collector.js +1 -1
  20. package/dist/compiler/fiber-compiler.d.ts +16 -30
  21. package/dist/compiler/fiber-compiler.d.ts.map +1 -1
  22. package/dist/compiler/fiber-compiler.js +32 -72
  23. package/dist/compiler/fiber-compiler.js.map +1 -1
  24. package/dist/compiler/index.d.ts +1 -1
  25. package/dist/compiler/index.js +1 -1
  26. package/dist/compiler/scheduler.d.ts +3 -3
  27. package/dist/compiler/scheduler.js +4 -4
  28. package/dist/compiler/scheduler.js.map +1 -1
  29. package/dist/component/component.d.ts +6 -6
  30. package/dist/component/component.d.ts.map +1 -1
  31. package/dist/hooks/com-state.d.ts +18 -4
  32. package/dist/hooks/com-state.d.ts.map +1 -1
  33. package/dist/hooks/com-state.js +44 -15
  34. package/dist/hooks/com-state.js.map +1 -1
  35. package/dist/hooks/context-info.d.ts +2 -35
  36. package/dist/hooks/context-info.d.ts.map +1 -1
  37. package/dist/hooks/context-info.js +8 -0
  38. package/dist/hooks/context-info.js.map +1 -1
  39. package/dist/hooks/context.d.ts +2 -3
  40. package/dist/hooks/context.d.ts.map +1 -1
  41. package/dist/hooks/context.js +2 -3
  42. package/dist/hooks/context.js.map +1 -1
  43. package/dist/hooks/data.d.ts +19 -2
  44. package/dist/hooks/data.d.ts.map +1 -1
  45. package/dist/hooks/data.js +14 -3
  46. package/dist/hooks/data.js.map +1 -1
  47. package/dist/hooks/formatter-context.d.ts +1 -2
  48. package/dist/hooks/formatter-context.d.ts.map +1 -1
  49. package/dist/hooks/formatter-context.js +1 -2
  50. package/dist/hooks/formatter-context.js.map +1 -1
  51. package/dist/hooks/index.d.ts +6 -4
  52. package/dist/hooks/index.d.ts.map +1 -1
  53. package/dist/hooks/index.js +6 -2
  54. package/dist/hooks/index.js.map +1 -1
  55. package/dist/hooks/message-context.d.ts +1 -1
  56. package/dist/hooks/message-context.js +1 -1
  57. package/dist/hooks/resolved.d.ts +2 -0
  58. package/dist/hooks/resolved.d.ts.map +1 -0
  59. package/dist/hooks/resolved.js +6 -0
  60. package/dist/hooks/resolved.js.map +1 -0
  61. package/dist/hooks/runtime-context.d.ts +46 -1
  62. package/dist/hooks/runtime-context.d.ts.map +1 -1
  63. package/dist/hooks/runtime-context.js +36 -1
  64. package/dist/hooks/runtime-context.js.map +1 -1
  65. package/dist/hooks/timeline.d.ts +10 -0
  66. package/dist/hooks/timeline.d.ts.map +1 -0
  67. package/dist/hooks/timeline.js +13 -0
  68. package/dist/hooks/timeline.js.map +1 -0
  69. package/dist/hooks/types.d.ts +1 -1
  70. package/dist/hooks/types.js +1 -1
  71. package/dist/index.d.ts +2 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/jsx/components/timeline.d.ts.map +1 -1
  76. package/dist/jsx/components/timeline.js +11 -11
  77. package/dist/jsx/components/timeline.js.map +1 -1
  78. package/dist/jsx/jsx-runtime.d.ts +1 -3
  79. package/dist/jsx/jsx-runtime.d.ts.map +1 -1
  80. package/dist/local-transport.d.ts +31 -0
  81. package/dist/local-transport.d.ts.map +1 -0
  82. package/dist/local-transport.js +119 -0
  83. package/dist/local-transport.js.map +1 -0
  84. package/dist/model/model.d.ts +0 -2
  85. package/dist/model/model.d.ts.map +1 -1
  86. package/dist/model/model.js.map +1 -1
  87. package/dist/procedure/index.d.ts.map +1 -1
  88. package/dist/reconciler/host-config.d.ts +6 -5
  89. package/dist/reconciler/host-config.d.ts.map +1 -1
  90. package/dist/reconciler/host-config.js +56 -27
  91. package/dist/reconciler/host-config.js.map +1 -1
  92. package/dist/reconciler/index.d.ts +1 -1
  93. package/dist/reconciler/index.js +1 -1
  94. package/dist/reconciler/reconciler.d.ts +12 -11
  95. package/dist/reconciler/reconciler.d.ts.map +1 -1
  96. package/dist/reconciler/reconciler.js +23 -22
  97. package/dist/reconciler/reconciler.js.map +1 -1
  98. package/dist/reconciler/types.d.ts +2 -8
  99. package/dist/reconciler/types.d.ts.map +1 -1
  100. package/dist/reconciler/types.js +2 -2
  101. package/dist/reconciler/types.js.map +1 -1
  102. package/dist/renderers/types.d.ts +1 -1
  103. package/dist/renderers/types.js +1 -1
  104. package/dist/testing/act.d.ts.map +1 -1
  105. package/dist/testing/act.js +2 -3
  106. package/dist/testing/act.js.map +1 -1
  107. package/dist/testing/index.d.ts +2 -0
  108. package/dist/testing/index.d.ts.map +1 -1
  109. package/dist/testing/index.js +2 -0
  110. package/dist/testing/index.js.map +1 -1
  111. package/dist/testing/mock-app.d.ts.map +1 -1
  112. package/dist/testing/mock-app.js +5 -15
  113. package/dist/testing/mock-app.js.map +1 -1
  114. package/dist/testing/mocks.d.ts +2 -3
  115. package/dist/testing/mocks.d.ts.map +1 -1
  116. package/dist/testing/mocks.js +2 -3
  117. package/dist/testing/mocks.js.map +1 -1
  118. package/dist/testing/render-agent.d.ts +1 -1
  119. package/dist/testing/render-agent.d.ts.map +1 -1
  120. package/dist/testing/render-agent.js +5 -5
  121. package/dist/testing/render-agent.js.map +1 -1
  122. package/dist/testing/test-environment.d.ts +122 -0
  123. package/dist/testing/test-environment.d.ts.map +1 -0
  124. package/dist/testing/test-environment.js +126 -0
  125. package/dist/testing/test-environment.js.map +1 -0
  126. package/package.json +15 -15
  127. package/dist/hibernation/index.d.ts +0 -126
  128. package/dist/hibernation/index.d.ts.map +0 -1
  129. package/dist/hibernation/index.js +0 -127
  130. package/dist/hibernation/index.js.map +0 -1
package/README.md CHANGED
@@ -40,7 +40,9 @@ function MyApp() {
40
40
  // Create and run
41
41
  const app = createApp(MyApp, { model: createOpenAIModel() });
42
42
  const session = await app.session();
43
- await session.send({ messages: [{ role: "user", content: [{ type: "text", text: "What is 2 + 2?" }] }] }).result;
43
+ await session.send({
44
+ messages: [{ role: "user", content: [{ type: "text", text: "What is 2 + 2?" }] }],
45
+ }).result;
44
46
  ```
45
47
 
46
48
  ## Level 0: `createAgent` (No JSX Required)
@@ -151,11 +153,11 @@ When `maxTokens` is set, Timeline automatically compacts entries that exceed the
151
153
 
152
154
  ```typescript
153
155
  interface TokenBudgetInfo {
154
- maxTokens: number; // configured budget
156
+ maxTokens: number; // configured budget
155
157
  effectiveBudget: number; // maxTokens - headroom
156
- currentTokens: number; // tokens in kept entries
157
- evictedCount: number; // entries dropped
158
- isCompacted: boolean; // whether compaction fired
158
+ currentTokens: number; // tokens in kept entries
159
+ evictedCount: number; // entries dropped
160
+ isCompacted: boolean; // whether compaction fired
159
161
  }
160
162
  ```
161
163
 
@@ -180,8 +182,7 @@ Group content with semantic meaning:
180
182
 
181
183
  ```tsx
182
184
  <Section id="context" title="Current Context">
183
- Today is {new Date().toDateString()}.
184
- User is logged in as {user.name}.
185
+ Today is {new Date().toDateString()}. User is logged in as {user.name}.
185
186
  </Section>
186
187
  ```
187
188
 
@@ -254,15 +255,15 @@ function MyComponent() {
254
255
  const counter = useSignal(0);
255
256
  const doubled = useComputed(() => counter() * 2, [counter]);
256
257
 
257
- counter(); // read: 0
258
- counter.set(5); // write
259
- counter.update(v => v + 1); // update with function
260
- doubled(); // read: 12
258
+ counter(); // read: 0
259
+ counter.set(5); // write
260
+ counter.update((v) => v + 1); // update with function
261
+ doubled(); // read: 12
261
262
 
262
263
  // COM state (persisted across ticks, shared between components)
263
264
  // Returns Signal<T>, NOT a tuple
264
265
  const notes = useComState<string[]>("notes", []);
265
- notes(); // read current value
266
+ notes(); // read current value
266
267
  notes.set(["a", "b"]); // write new value
267
268
  }
268
269
  ```
@@ -272,7 +273,14 @@ function MyComponent() {
272
273
  All lifecycle hooks follow the pattern: data first, COM (context) last.
273
274
 
274
275
  ```tsx
275
- import { useOnMount, useOnUnmount, useOnTickStart, useOnTickEnd, useAfterCompile, useContinuation } from "@agentick/core";
276
+ import {
277
+ useOnMount,
278
+ useOnUnmount,
279
+ useOnTickStart,
280
+ useOnTickEnd,
281
+ useAfterCompile,
282
+ useContinuation,
283
+ } from "@agentick/core";
276
284
 
277
285
  function MyComponent() {
278
286
  // Called when component mounts
@@ -304,7 +312,7 @@ function MyComponent() {
304
312
  useContinuation((result) => {
305
313
  // Return true to continue, false to stop
306
314
  if (result.text?.includes("<DONE>")) return false;
307
- if (result.tick >= 10) return false; // Safety limit
315
+ if (result.tick >= 10) return false; // Safety limit
308
316
  return true;
309
317
  });
310
318
 
@@ -376,7 +384,9 @@ function Agent() {
376
384
  const [temp] = useKnob("temp", 0.7, {
377
385
  description: "Temperature",
378
386
  group: "Model",
379
- min: 0, max: 2, step: 0.1,
387
+ min: 0,
388
+ max: 2,
389
+ step: 0.1,
380
390
  });
381
391
 
382
392
  // Boolean → model sees [toggle] type
@@ -496,13 +506,13 @@ function ContextAwareComponent() {
496
506
  ```typescript
497
507
  interface ContextInfo {
498
508
  // Model identification
499
- modelId: string; // "gpt-4o", "claude-3-5-sonnet", etc.
500
- modelName?: string; // Human-readable name
501
- provider?: string; // "openai", "anthropic", etc.
509
+ modelId: string; // "gpt-4o", "claude-3-5-sonnet", etc.
510
+ modelName?: string; // Human-readable name
511
+ provider?: string; // "openai", "anthropic", etc.
502
512
 
503
513
  // Context limits
504
- contextWindow?: number; // Total context window size
505
- maxOutputTokens?: number; // Max output tokens for model
514
+ contextWindow?: number; // Total context window size
515
+ maxOutputTokens?: number; // Max output tokens for model
506
516
 
507
517
  // Token usage (current tick)
508
518
  inputTokens: number;
@@ -510,7 +520,7 @@ interface ContextInfo {
510
520
  totalTokens: number;
511
521
 
512
522
  // Utilization
513
- utilization?: number; // Percentage (0-100)
523
+ utilization?: number; // Percentage (0-100)
514
524
 
515
525
  // Model capabilities
516
526
  supportsVision?: boolean;
@@ -518,7 +528,7 @@ interface ContextInfo {
518
528
  isReasoningModel?: boolean;
519
529
 
520
530
  // Execution info
521
- tick: number; // Current tick number
531
+ tick: number; // Current tick number
522
532
 
523
533
  // Cumulative usage across all ticks
524
534
  cumulativeUsage?: {
@@ -543,7 +553,7 @@ const contextInfoStore = createContextInfoStore();
543
553
  // Provide to components
544
554
  <ContextInfoProvider store={contextInfoStore}>
545
555
  <MyApp />
546
- </ContextInfoProvider>
556
+ </ContextInfoProvider>;
547
557
 
548
558
  // Update the store
549
559
  contextInfoStore.update({
@@ -579,11 +589,7 @@ const WeatherTool = createTool({
579
589
  return [{ type: "text", text: JSON.stringify(weather) }];
580
590
  },
581
591
  // Optional: render state to model context (receives tickState, ctx)
582
- render: (tickState, ctx) => (
583
- <Section id="weather-info">
584
- Last checked: {lastChecked}
585
- </Section>
586
- ),
592
+ render: (tickState, ctx) => <Section id="weather-info">Last checked: {lastChecked}</Section>,
587
593
  });
588
594
 
589
595
  // Use in your app
@@ -607,7 +613,7 @@ import { createApp } from "@agentick/core";
607
613
 
608
614
  const app = createApp(MyApp, {
609
615
  model: myModel,
610
- devTools: true, // Enable DevTools
616
+ devTools: true, // Enable DevTools
611
617
  });
612
618
  ```
613
619
 
@@ -620,6 +626,7 @@ const app = createApp(MyApp, {
620
626
  devTools: true, // Enable DevTools emission
621
627
  tools: [ExternalTool], // Additional tools (merged with JSX <Tool>s)
622
628
  mcpServers: { ... }, // MCP server configs
629
+ environment: myEnv, // Execution environment (see below)
623
630
  });
624
631
  ```
625
632
 
@@ -638,11 +645,17 @@ const app = createApp(MyApp, {
638
645
  onError: (error) => console.error(error),
639
646
 
640
647
  // All events (fine-grained)
641
- onEvent: (event) => { /* handle any stream event */ },
648
+ onEvent: (event) => {
649
+ /* handle any stream event */
650
+ },
642
651
 
643
652
  // Send lifecycle
644
- onBeforeSend: (session, input) => { /* modify input */ },
645
- onAfterSend: (session, result) => { /* post-processing */ },
653
+ onBeforeSend: (session, input) => {
654
+ /* modify input */
655
+ },
656
+ onAfterSend: (session, result) => {
657
+ /* post-processing */
658
+ },
646
659
 
647
660
  // Tool confirmation
648
661
  onToolConfirmation: async (call, message) => {
@@ -689,18 +702,17 @@ const handle = await session.spawn(
689
702
  );
690
703
 
691
704
  // Spawn with a JSX element (props from element + input.props are merged)
692
- const handle = await session.spawn(
693
- <Researcher query="quantum computing" />,
694
- { messages: [{ role: "user", content: [{ type: "text", text: "Go" }] }] },
695
- );
705
+ const handle = await session.spawn(<Researcher query="quantum computing" />, {
706
+ messages: [{ role: "user", content: [{ type: "text", text: "Go" }] }],
707
+ });
696
708
  ```
697
709
 
698
710
  **Parallel spawns** work with `Promise.all`:
699
711
 
700
712
  ```tsx
701
713
  const [researchResult, factCheckResult] = await Promise.all([
702
- session.spawn(Researcher, { messages }).then(h => h.result),
703
- session.spawn(FactChecker, { messages }).then(h => h.result),
714
+ session.spawn(Researcher, { messages }).then((h) => h.result),
715
+ session.spawn(FactChecker, { messages }).then((h) => h.result),
704
716
  ]);
705
717
  ```
706
718
 
@@ -730,34 +742,138 @@ const DelegateTool = createTool({
730
742
  - **Depth limit**: Maximum 10 levels of nesting (throws if exceeded).
731
743
  - **Cleanup**: Children are removed from `session.children` when they complete.
732
744
 
733
- ### Session Persistence & Hibernation
745
+ ### Session Persistence
734
746
 
735
- Control hibernation, limits, and auto-cleanup:
747
+ Sessions auto-persist after each execution and auto-restore when accessed via `app.session(id)`.
736
748
 
737
749
  ```typescript
738
750
  const app = createApp(MyApp, {
739
751
  model,
740
752
  sessions: {
741
- store: new RedisSessionStore(redis), // Or ":memory:" for SQLite
742
- maxActive: 100, // Max concurrent sessions
743
- idleTimeout: 5 * 60 * 1000, // Hibernate after 5 min idle
744
- autoHibernate: true, // Auto-hibernate on idle
753
+ store: "./data/sessions.db", // SQLite file (or ':memory:', or custom SessionStore)
754
+ maxActive: 100, // Max concurrent in-memory sessions
755
+ idleTimeout: 5 * 60 * 1000, // Evict from memory after 5 min idle
745
756
  },
757
+ });
758
+ ```
759
+
760
+ **How it works:**
761
+
762
+ 1. After each execution, session state is auto-saved to the store (fire-and-forget — persist failures don't block execution)
763
+ 2. When `app.session("user-123")` is called and the session isn't in memory, it's auto-restored from the store
764
+ 3. `useComState` and `useData` values are included in snapshots by default (set `{ persist: false }` to exclude)
765
+ 4. `maxActive` and `idleTimeout` control memory — evicted sessions can be restored from store
746
766
 
747
- // Session lifecycle hooks
748
- onSessionCreate: (session) => { /* ... */ },
749
- onSessionClose: (sessionId) => { /* ... */ },
767
+ #### Snapshot Contents
750
768
 
751
- // Hibernation hooks
752
- onBeforeHibernate: (session, snapshot) => {
753
- // Return false to cancel, modified snapshot, or void
754
- if (session.inspect().lastToolCalls.length > 0) return false;
769
+ A `SessionSnapshot` captures:
770
+
771
+ | Field | Type | Description |
772
+ | ----------- | --------------------------- | ------------------------------------------------------ |
773
+ | `timeline` | `COMTimelineEntry[] ∣ null` | Full conversation history |
774
+ | `comState` | `Record<string, unknown>` | All `useComState` values (with `persist !== false`) |
775
+ | `dataCache` | `Record<string, ...>` | All `useData` cached values (with `persist !== false`) |
776
+ | `tick` | `number` | Tick count at snapshot time |
777
+ | `usage` | `UsageStats` | Accumulated token usage |
778
+
779
+ #### Lifecycle Hooks
780
+
781
+ ```typescript
782
+ const app = createApp(MyApp, {
783
+ model,
784
+ sessions: { store: "./sessions.db" },
785
+
786
+ // Before save — cancel or modify snapshot
787
+ onBeforePersist: (session, snapshot) => {
788
+ if (snapshot.tick < 2) return false; // Don't persist short sessions
789
+ },
790
+
791
+ // After save
792
+ onAfterPersist: (sessionId, snapshot) => {
793
+ console.log(`Saved session ${sessionId}`);
755
794
  },
756
- onAfterHibernate: (sessionId, snapshot) => { /* ... */ },
757
- onBeforeHydrate: (sessionId, snapshot) => {
758
- // Migrate old formats, validate, etc.
795
+
796
+ // Before restore migrate old formats
797
+ onBeforeRestore: (sessionId, snapshot) => {
798
+ if (snapshot.version !== "1.0") return migrateSnapshot(snapshot);
799
+ },
800
+
801
+ // After restore
802
+ onAfterRestore: (session, snapshot) => {
803
+ console.log(`Restored session ${session.id} at tick ${snapshot.tick}`);
759
804
  },
760
- onAfterHydrate: (session, snapshot) => { /* ... */ },
805
+ });
806
+ ```
807
+
808
+ #### Restore Layers
809
+
810
+ **Layer 1 (default):** Snapshot data is auto-applied. Timeline, comState, and dataCache are restored directly. Components see their previous state via `useComState` and `useData`.
811
+
812
+ **Layer 2 (resolve):** When `resolve` is configured, auto-apply is disabled. Resolve functions control reconstruction and receive the snapshot as context. Results are available via `useResolved(key)`.
813
+
814
+ ```typescript
815
+ const app = createApp(MyApp, {
816
+ model,
817
+ sessions: { store: "./sessions.db" },
818
+
819
+ // Layer 2: resolve controls reconstruction
820
+ resolve: {
821
+ greeting: (ctx) => `Welcome back! You were on tick ${ctx.snapshot?.tick}`,
822
+ userData: async (ctx) => fetchUser(ctx.sessionId),
823
+ },
824
+ });
825
+
826
+ // In components:
827
+ function MyAgent() {
828
+ const greeting = useResolved<string>("greeting");
829
+ const userData = useResolved<User>("userData");
830
+ // ...
831
+ }
832
+ ```
833
+
834
+ #### Context Management vs History
835
+
836
+ The session's `_timeline` is the append-only historical log — it grows with every message. The `<Timeline>` component controls what the model _sees_ (context) via its props:
837
+
838
+ ```tsx
839
+ // Full history → model context (default)
840
+ <Timeline />
841
+
842
+ // Only recent messages in context
843
+ <Timeline limit={20} />
844
+
845
+ // Token-budgeted context
846
+ <Timeline maxTokens={8000} strategy="sliding-window" headroom={500} />
847
+
848
+ // Role filtering
849
+ <Timeline roles={['user', 'assistant']} />
850
+ ```
851
+
852
+ The `useTimeline()` hook provides direct access for advanced patterns:
853
+
854
+ ```tsx
855
+ function MyAgent() {
856
+ const timeline = useTimeline();
857
+
858
+ // Read current entries
859
+ console.log(timeline.entries.length);
860
+
861
+ // Replace timeline (e.g., after summarization)
862
+ timeline.set([summaryEntry, ...recentEntries]);
863
+
864
+ // Transform timeline
865
+ timeline.update((entries) => entries.filter((e) => e.message.role !== "system"));
866
+ }
867
+ ```
868
+
869
+ #### `maxTimelineEntries` — OOM Safety Net
870
+
871
+ For long-running sessions, `maxTimelineEntries` prevents unbounded memory growth by trimming the oldest entries after each tick. This is a safety net, not a context management strategy — use `<Timeline>` props for context control.
872
+
873
+ ```typescript
874
+ const app = createApp(MyApp, {
875
+ model,
876
+ maxTimelineEntries: 500, // Keep at most 500 entries in memory
761
877
  });
762
878
  ```
763
879
 
@@ -802,8 +918,8 @@ Apps inherit from the global `Agentick` instance by default:
802
918
  import { Agentick, createApp } from "@agentick/core";
803
919
 
804
920
  // Register global middleware
805
- Agentick.use('*', loggingMiddleware);
806
- Agentick.use('tool:*', authMiddleware);
921
+ Agentick.use("*", loggingMiddleware);
922
+ Agentick.use("tool:*", authMiddleware);
807
923
 
808
924
  // App inherits global middleware (default)
809
925
  const app = createApp(MyApp, { model });
@@ -811,7 +927,7 @@ const app = createApp(MyApp, { model });
811
927
  // Isolated app (for testing)
812
928
  const testApp = createApp(TestApp, {
813
929
  model,
814
- inheritDefaults: false
930
+ inheritDefaults: false,
815
931
  });
816
932
  ```
817
933
 
@@ -822,10 +938,10 @@ For one-off executions without session management:
822
938
  ```tsx
823
939
  import { run } from "@agentick/core";
824
940
 
825
- const result = await run(
826
- <MyApp />,
827
- { messages: [{ role: "user", content: [{ type: "text", text: "Hello!" }] }], model: myModel }
828
- );
941
+ const result = await run(<MyApp />, {
942
+ messages: [{ role: "user", content: [{ type: "text", text: "Hello!" }] }],
943
+ model: myModel,
944
+ });
829
945
  ```
830
946
 
831
947
  ### Choosing `run()` vs `createApp`
@@ -843,7 +959,79 @@ const result = await run(
843
959
  await run(<Agent query="default" />, { props: { query: "override" }, model, messages });
844
960
  ```
845
961
 
846
- `createApp` takes a component function and returns a reusable app with session management, hibernation, and middleware support.
962
+ `createApp` takes a component function and returns a reusable app with session management, persistence, and middleware support.
963
+
964
+ ## Execution Environments
965
+
966
+ An `ExecutionEnvironment` controls the execution backend — how compiled context reaches the model and how tool calls are routed. The default is the standard model → tool_use protocol. Swap in a different environment to change the entire execution model without touching your agent code.
967
+
968
+ ```tsx
969
+ import { createApp, type ExecutionEnvironment } from "@agentick/core";
970
+
971
+ const replEnvironment: ExecutionEnvironment = {
972
+ name: "repl",
973
+
974
+ // Transform what the model sees — replace tool schemas with prose descriptions,
975
+ // expose a single "execute" tool, restructure sections, anything.
976
+ prepareModelInput(compiled, tools) {
977
+ const commandList = tools
978
+ .map((t) => `- ${t.metadata?.name}: ${t.metadata?.description}`)
979
+ .join("\n");
980
+ return {
981
+ ...compiled,
982
+ tools: [executeToolSchema],
983
+ system: [...compiled.system, { content: `Available commands:\n${commandList}` }],
984
+ };
985
+ },
986
+
987
+ // Route tool calls — intercept "execute" to a sandbox, let others pass through.
988
+ async executeToolCall(call, tool, next) {
989
+ if (call.name === "execute") {
990
+ return sandbox.run(call.input.code);
991
+ }
992
+ return next();
993
+ },
994
+
995
+ // Session lifecycle
996
+ onSessionInit(session) {
997
+ sandbox.create(session.id);
998
+ },
999
+ onDestroy(session) {
1000
+ sandbox.destroy(session.id);
1001
+ },
1002
+ onPersist(session, snapshot) {
1003
+ return { ...snapshot, comState: { ...snapshot.comState, _sandbox: sandbox.state() } };
1004
+ },
1005
+ onRestore(session, snapshot) {
1006
+ sandbox.restore(snapshot.comState._sandbox);
1007
+ },
1008
+ };
1009
+
1010
+ // Same agent, different execution model
1011
+ const app = createApp(MyAgent, { model, environment: replEnvironment });
1012
+ ```
1013
+
1014
+ The agent's JSX — its `<System>`, `<Timeline>`, `<Tool>` components — stays identical. The environment transforms how that compiled context is consumed and how tool calls execute. This means you can build one agent and run it against multiple backends: standard tool_use for production, a sandboxed REPL for code execution, a human-in-the-loop gateway for approval workflows.
1015
+
1016
+ ### Interface
1017
+
1018
+ All methods are optional. Omitted methods use default behavior.
1019
+
1020
+ | Hook | Purpose | Timing |
1021
+ | ------------------- | --------------------------------------------------- | ------------- |
1022
+ | `prepareModelInput` | Transform compiled context before the model sees it | Per tick |
1023
+ | `executeToolCall` | Intercept, transform, or replace tool execution | Per tool call |
1024
+ | `onSessionInit` | Set up per-session resources (sandbox, workspace) | Once |
1025
+ | `onPersist` | Add environment state to session snapshot | Per save |
1026
+ | `onRestore` | Restore environment state from snapshot | Once |
1027
+ | `onDestroy` | Clean up resources | Once |
1028
+
1029
+ ### Use Cases
1030
+
1031
+ - **REPL/Code Execution**: Replace tool schemas with command descriptions, route `execute` calls to a sandboxed runtime, persist sandbox state across sessions.
1032
+ - **Human-in-the-Loop**: Transform tool calls into approval requests, gate execution on human confirmation, log decisions.
1033
+ - **Sandboxing**: Run tools in isolated containers, inject security boundaries, audit tool invocations.
1034
+ - **Testing**: Intercept specific tools to return canned responses, track all lifecycle calls for assertions. See `createTestEnvironment()` in `@agentick/core/testing`.
847
1035
 
848
1036
  ## DevTools Integration
849
1037
 
@@ -867,9 +1055,27 @@ For debugging the reconciler itself with React DevTools:
867
1055
  import { enableReactDevTools } from "@agentick/core";
868
1056
 
869
1057
  // Before creating sessions
870
- enableReactDevTools(); // Connects to npx react-devtools on port 8097
1058
+ enableReactDevTools(); // Connects to npx react-devtools on port 8097
1059
+ ```
1060
+
1061
+ ## Local Transport
1062
+
1063
+ `createLocalTransport(app)` bridges an in-process `App` to the `ClientTransport` interface. This enables `@agentick/client` (and `@agentick/react` hooks) to work with a local app without any network layer.
1064
+
1065
+ ```typescript
1066
+ import { createApp } from "@agentick/core";
1067
+ import { createLocalTransport } from "@agentick/core";
1068
+ import { createClient } from "@agentick/client";
1069
+
1070
+ const app = createApp(MyAgent, { model });
1071
+ const transport = createLocalTransport(app);
1072
+ const client = createClient({ baseUrl: "local://", transport });
871
1073
  ```
872
1074
 
1075
+ The transport is always "connected" — there's no network. `send()` delegates to `app.send()` and streams `SessionExecutionHandle` events as `TransportEventData`. Used by `@agentick/tui` for local agent mode.
1076
+
1077
+ See [`packages/shared/src/transport.ts`](../shared/src/transport.ts) for the `ClientTransport` interface.
1078
+
873
1079
  ## License
874
1080
 
875
1081
  MIT