# GTPyhop Thread-Safe Sessions Guide

GTPyhop 1.3.0 introduced a session-based, thread-safe architecture. GTPyhop 1.8.0 adds memory tracking integration. GTPyhop 1.9.0 adds the iterative DFS backtracking strategy and the `strategy` parameter. This guide explains why sessions matter, how to use them, and shows concurrent examples.

## Why Thread-Safe Sessions?

- **Isolation of global state**: Pre-1.3.0 workflows depended on process-global state (e.g., `current_domain`, verbosity, planning strategy). In concurrent code, runs could interfere with each other.
- **Reliable concurrency**: Each `PlannerSession` has its own configuration, lock, logs, and stats; concurrent planning in threads is safe starting with 1.3.0.
- **Per-session control**: Set per-session verbosity, planning strategy (recursive DFS, iterative greedy, or iterative DFS backtracking), timeouts, and memory tracking. Persist and restore sessions independently.

Key APIs in `gtpyhop` (1.3.0+): `PlannerSession`, `create_session`, `get_session`, `destroy_session`, `list_sessions`, `PlanningTimeoutError`, `SessionSerializer`, `restore_session`, `restore_all_sessions`.

## Basic Session Example

Below is the same logic from `README.md` (Very first HTN example), expressed with the session-based API:

```python
import gtpyhop

# 1) Domain creation
my_domain = gtpyhop.Domain('my_domain')

# 2) Define a state
state = gtpyhop.State('initial_state')
state.pos = {'obj1': 'loc1', 'obj2': 'loc2'}

# 3) Actions
def move(state, obj, target):
    if obj in state.pos:
        state.pos[obj] = target
        return state
    return False

gtpyhop.declare_actions(move)

# 4) Task methods
def transport(state, obj, destination):
    current = state.pos[obj]
    if current != destination:
        return [('move', obj, destination)]
    return []

gtpyhop.declare_task_methods('transport', transport)

# 5) Plan using a session (1.3.0+)
with gtpyhop.PlannerSession(domain=my_domain, verbose=1) as session:
    with session.isolated_execution():
        result = session.find_plan(state, [('transport', 'obj1', 'loc2')])
        if result.success:
            print(result.plan)
        else:
            print('Planning failed:', result.error)
```

Notes:
- `PlannerSession(domain=...)` keeps the planning isolated. The `isolated_execution()` context manager safely sets and restores global knobs during the call.
- You can also pass session-specific controls, e.g. `recursive=True` for the recursive strategy, `strategy="iterative_dfs_backtracking"` for the iterative backtracking strategy (1.9.0+), or a `timeout_ms` to `find_plan`.

Example with a timeout:

```python
result = session.find_plan(state, [('transport', 'obj1', 'loc2')], timeout_ms=500)
```

## Session with Memory Tracking (1.8.0+)

GTPyhop 1.8.0 adds memory tracking integration:

```python
with gtpyhop.PlannerSession(
    domain=my_domain,
    verbose=1,
    memory_tracking=True,
    memory_sampling_interval=0.1  # 100ms sampling
) as session:
    with session.isolated_execution():
        result = session.find_plan(state, tasks)
        if result.success:
            print(f"Plan: {result.plan}")
            print(f"Memory used: {result.stats['memory_mb']:.2f} MB")
            print(f"Peak memory: {result.stats['peak_memory_mb']:.2f} MB")
```

## Planning Strategy Selection (1.9.0+)

GTPyhop 1.9.0 supports three planning strategies. The `strategy` parameter (new in 1.9.0) takes precedence over the legacy `recursive` bool when both are provided.

| Strategy name | Backtracking? | Stack | `PlannerSession` parameter |
|---------------|:------------:|-------|----------------------------|
| Recursive DFS | Yes | Python call stack | `recursive=True` or `strategy="recursive_dfs"` |
| Iterative greedy | No | Explicit stack | `recursive=False` (default) or `strategy="iterative_greedy"` |
| Iterative DFS BT | Yes | Explicit stack | `strategy="iterative_dfs_backtracking"` |

```python
# Iterative DFS with backtracking (1.9.0+)
with gtpyhop.PlannerSession(
    domain=my_domain,
    strategy="iterative_dfs_backtracking"
) as session:
    result = session.find_plan(state, tasks)

# Legacy interface still works identically
with gtpyhop.PlannerSession(domain=my_domain, recursive=True) as session:
    result = session.find_plan(state, tasks)  # recursive DFS

with gtpyhop.PlannerSession(domain=my_domain, recursive=False) as session:
    result = session.find_plan(state, tasks)  # iterative greedy
```

When `strategy` is provided, `recursive` is ignored. The `session.recursive` property remains available and returns `True` only when the strategy is `"recursive_dfs"`.

The strategy name is reported in `result.stats["strategy"]` (e.g. `"iterative_dfs_backtracking"`).

## Concurrent Planning Example

Two sessions plan in parallel, each with its own Domain and verbosity. Before 1.3.0, mutating globals concurrently risked races and cross-talk between runs.

```python
import threading
import gtpyhop

# Domain A
A = gtpyhop.Domain('A')
stateA = gtpyhop.State('sA'); stateA.pos = {'x': 'l1'}

def moveA(s, o, t):
    if o in s.pos: s.pos[o] = t; return s
    return False

gtpyhop.declare_actions(moveA)

def taskA(s, o, d):
    return [('moveA', o, d)] if s.pos[o] != d else []

gtpyhop.declare_task_methods('taskA', taskA)

# Domain B
B = gtpyhop.Domain('B')
stateB = gtpyhop.State('sB'); stateB.pos = {'y': 'm1'}

def moveB(s, o, t):
    if o in s.pos: s.pos[o] = t; return s
    return False

gtpyhop.declare_actions(moveB)

def taskB(s, o, d):
    return [('moveB', o, d)] if s.pos[o] != d else []

gtpyhop.declare_task_methods('taskB', taskB)

plans = {}

def worker(name, domain, state, todo):
    with gtpyhop.PlannerSession(domain=domain, verbose=2) as session:
        with session.isolated_execution():
            result = session.find_plan(state, todo)
            plans[name] = result.plan if result.success else result.error

threads = [
    threading.Thread(target=worker, args=('A', A, stateA, [('taskA', 'x', 'l2')])),
    threading.Thread(target=worker, args=('B', B, stateB, [('taskB', 'y', 'm2')]))
]

[t.start() for t in threads]
[t.join() for t in threads]

print(plans)  # {'A': [('moveA', 'x', 'l2')], 'B': [('moveB', 'y', 'm2')]}
```

### Why This Is Unsafe Without Sessions (Pre-1.3.0)

Concurrent use of the classic global API is effectively unsafe:

- **Domain swapping contamination:** Thread A sets `current_domain = A`; before `find_plan` completes, Thread B sets `current_domain = B`. A's planner may pick B's methods, producing invalid plans.
- **Strategy/verbosity races:** One thread toggles `set_recursive_planning(True)` while another is planning, leading to nondeterministic behavior.

## Session APIs Reference

### Core Session APIs (1.3.0+)

| API | Description |
|-----|-------------|
| `PlannerSession(domain, verbose, ...)` | Isolated planning context |
| `PlannerSession(strategy=...)` | Strategy selection: `"recursive_dfs"`, `"iterative_greedy"`, `"iterative_dfs_backtracking"` (1.9.0+) |
| `session.isolated_execution()` | Context manager for safe execution |
| `session.find_plan(state, tasks, timeout_ms)` | Plan with optional timeout |
| `create_session(session_id, **kwargs)` | Create and register a session |
| `get_session(session_id)` | Fetch existing session |
| `destroy_session(session_id)` | Cleanup and remove session |
| `list_sessions()` | Enumerate active sessions |
| `PlanningTimeoutError` | Exception for timeout |

### Persistence APIs (1.3.0+)

| API | Description |
|-----|-------------|
| `SessionSerializer` | Serialize/deserialize sessions |
| `restore_session(session_id)` | Restore a saved session |
| `restore_all_sessions()` | Restore all saved sessions |
| `set_persistence_directory(path)` | Configure auto-save location |
| `get_persistence_directory()` | Get persistence location |

### Memory Tracking APIs (1.8.0+)

| API | Description |
|-----|-------------|
| `memory_tracking` | Enable memory tracking (default: `False`) |
| `memory_sampling_interval` | Sampling interval: 0.1s default, use 0.001s for fast scenarios (<100ms) |
| `result.stats['memory_mb']` | Memory used during planning |
| `result.stats['peak_memory_mb']` | Peak memory observed |

## Dual-Mode Interface

All core examples support both legacy and session modes:

```bash
# Legacy mode (backward compatible)
python -m gtpyhop.examples.simple_htn

# Session mode (thread-safe, recommended)
python -m gtpyhop.examples.simple_htn --session

# Session mode with custom settings
python -m gtpyhop.examples.simple_htn --session --verbose 3 --no-pauses
```

**Available arguments:**
- `--session`: Enable thread-safe session mode
- `--verbose N`: Set verbosity level (0-3)
- `--no-pauses`: Skip interactive pauses

## When to Use Sessions

Use sessions when:
- Running planners concurrently (threads/processes) or from a web/API server
- Needing per-run settings (verbosity, planning strategy) without affecting others
- Requiring timeouts/cancellation, structured logs, or persistence per run
- Tracking memory usage during planning (1.8.0+)

## Related Documentation

- [Running Examples](https://github.com/PCfVW/GTPyhop/blob/pip/docs/running_examples.md) - How to run all examples
- [Structured Logging](https://github.com/PCfVW/GTPyhop/blob/pip/docs/logging.md) - Logging and memory tracking
- [All Examples Guide](https://github.com/PCfVW/GTPyhop/blob/pip/docs/all_examples.md) - Detailed example documentation
