multi-forge 0.2.0__py3-none-any.whl
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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
forge/session/models.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Dataclasses for Forge Session module.
|
|
2
|
+
|
|
3
|
+
All timestamps are stored as ISO8601 strings for trivial JSON roundtripping.
|
|
4
|
+
Use forge.core.state.now_iso() to generate timestamps and parse_iso()
|
|
5
|
+
for runtime conversion.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from forge.core.state import now_iso
|
|
14
|
+
from forge.guard.team.config import TeamSupervisorConfig
|
|
15
|
+
from forge.guard.types import FailMode
|
|
16
|
+
|
|
17
|
+
from .config import LAUNCH_MODE_HOST, LAUNCH_MODE_SIDECAR
|
|
18
|
+
|
|
19
|
+
# Schema version for session state files.
|
|
20
|
+
SCHEMA_VERSION = 1
|
|
21
|
+
INDEX_VERSION = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- Worktree metadata (embedded in SessionState) ---
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Worktree:
|
|
29
|
+
"""Git worktree metadata.
|
|
30
|
+
|
|
31
|
+
``path`` is the checkout root (git ``--show-toplevel``), not the Forge
|
|
32
|
+
project root. The Forge project root is stored separately as
|
|
33
|
+
``SessionState.forge_root`` and ``SessionIndexEntry.forge_root``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
path: str # Absolute path to checkout root (git --show-toplevel)
|
|
37
|
+
branch: str # Git branch name (may contain slashes)
|
|
38
|
+
is_worktree: bool = False # True only if this is a git worktree (not main repo)
|
|
39
|
+
owns_worktree: bool = True # False for --into (session is a guest, not the creator)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- Intent section - what Forge requested ---
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ProxyIntent:
|
|
47
|
+
"""Proxy configuration intent. Both fields are required."""
|
|
48
|
+
|
|
49
|
+
template: str # e.g., "litellm-gemini"
|
|
50
|
+
base_url: str # e.g., "http://localhost:8084"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SystemPromptIntent:
|
|
55
|
+
"""System prompt configuration intent."""
|
|
56
|
+
|
|
57
|
+
mode: str = "append" # "append" or "replace"
|
|
58
|
+
file: str | None = None # Path to custom system prompt file
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class SidecarLaunchIntent:
|
|
63
|
+
"""Persisted sidecar launch preferences for reproducible relaunches."""
|
|
64
|
+
|
|
65
|
+
mounts: list[str] = field(default_factory=list) # Raw CLI mount specs: host:container[:ro|rw]
|
|
66
|
+
image: str | None = None # Optional sidecar image override
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class LaunchIntent:
|
|
71
|
+
"""How Forge should relaunch this session."""
|
|
72
|
+
|
|
73
|
+
mode: str = LAUNCH_MODE_HOST # "host" or "sidecar"
|
|
74
|
+
sidecar: SidecarLaunchIntent | None = None
|
|
75
|
+
direct_model: str | None = None # Claude Code env-ready direct model pin (e.g. claude-opus-4-7[1m])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class HandoffConfig:
|
|
80
|
+
"""Handoff agent configuration for automatic memory doc updates.
|
|
81
|
+
|
|
82
|
+
The handoff agent runs after session stop to update designated
|
|
83
|
+
project memory documents (e.g., project-state.md) using ``claude -p``.
|
|
84
|
+
|
|
85
|
+
Fields:
|
|
86
|
+
enabled: Whether the handoff agent should run on session stop.
|
|
87
|
+
mode: "augment" (add missing info) or "review-only" (report only, no edits).
|
|
88
|
+
proxy: Optional proxy (proxy_id or template name) to route the agent's
|
|
89
|
+
LLM calls through. If None, inherits the session's confirmed proxy.
|
|
90
|
+
direct: When True, force direct Anthropic routing regardless of session proxy.
|
|
91
|
+
min_turns: Minimum conversation turns before triggering handoff.
|
|
92
|
+
Sessions below this threshold are skipped (too short to be useful).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
enabled: bool = False
|
|
96
|
+
mode: str = "augment" # "augment" | "review-only"
|
|
97
|
+
proxy: str | None = None
|
|
98
|
+
direct: bool = False
|
|
99
|
+
min_turns: int = 5
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class DesignatedDoc:
|
|
104
|
+
"""A document the handoff agent should update after session stop.
|
|
105
|
+
|
|
106
|
+
Fields:
|
|
107
|
+
path: Worktree-relative path (e.g., "docs/checklist.md").
|
|
108
|
+
Must NOT be absolute. Resolved against worktree_path at runtime.
|
|
109
|
+
strategy: Built-in augmentation strategy:
|
|
110
|
+
"project-state" — handoff notes (skip if missing)
|
|
111
|
+
"checklist" — mark completed tasks, add discovered tasks
|
|
112
|
+
"changelog" — add accomplishments not already recorded
|
|
113
|
+
"debugging" — record error causes, solutions, workarounds
|
|
114
|
+
"patterns" — record architecture patterns and conventions
|
|
115
|
+
"suggested" — propose additions as checkboxes (requires shadows)
|
|
116
|
+
"generic" — read and add missing information (default)
|
|
117
|
+
Unknown values fall back to "generic" behavior.
|
|
118
|
+
shadows: When set, switches to shadow/propose mode (Mode 2).
|
|
119
|
+
Path to the official document this doc proposes changes for.
|
|
120
|
+
The agent reads the official doc first, then writes suggestions
|
|
121
|
+
to this doc's path. Only valid with strategy="suggested".
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
path: str
|
|
125
|
+
strategy: str = "generic"
|
|
126
|
+
shadows: str | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class MemoryIntent:
|
|
131
|
+
"""Memory/context injection intent."""
|
|
132
|
+
|
|
133
|
+
auto_recall: bool = False
|
|
134
|
+
tags: list[str] = field(default_factory=list)
|
|
135
|
+
strategy: str = "summary" # "summary", "full", or "off"
|
|
136
|
+
max_chars: int = 6000
|
|
137
|
+
generated_file: str | None = None # e.g., ".claude/forge.context.generated.md"
|
|
138
|
+
designated_docs: list[DesignatedDoc] = field(default_factory=list)
|
|
139
|
+
auto_update: HandoffConfig | None = None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class SupervisorConfig:
|
|
144
|
+
"""Semantic supervisor configuration.
|
|
145
|
+
|
|
146
|
+
The supervisor is an LLM session (typically forked from the planner) that
|
|
147
|
+
validates executor actions against the approved plan.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
resume_id: str | None = None # Claude session UUID, or a Forge session name resolved to a UUID at runtime
|
|
151
|
+
proxy: str | None = None # Optional: proxy_id or template name for base_url lookup
|
|
152
|
+
direct: bool = False # When True, force direct Anthropic routing
|
|
153
|
+
base_url: str | None = None # Optional: explicit base_url override
|
|
154
|
+
forge_root: str | None = None # Scope for name-based lookups (set at wiring time)
|
|
155
|
+
timeout_seconds: int = 45 # Max time to wait for supervisor response (15s margin within 60s hook timeout)
|
|
156
|
+
throttle_seconds: int = 30 # Min time between supervisor calls (for caching)
|
|
157
|
+
fork_session: bool = True # Fork supervisor session to avoid polluting planner context
|
|
158
|
+
suspended: bool = False # True = supervision paused, config preserved
|
|
159
|
+
plan_override_path: str | None = None # Absolute path to plan file that supersedes session context
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class PolicyIntent:
|
|
164
|
+
"""Policy configuration for the session.
|
|
165
|
+
|
|
166
|
+
Policies are enforced at PreToolUse:Write/Edit boundaries. They can be
|
|
167
|
+
deterministic (fast local checks) or semantic (LLM-based supervisor).
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
enabled: bool = False
|
|
171
|
+
fail_mode: FailMode = "open" # "open" = allow on error, "closed" = deny on error
|
|
172
|
+
bundles: list[str] = field(default_factory=list) # e.g., ["tdd", "coding_standards"]
|
|
173
|
+
bundle_config: dict[str, dict[str, Any]] = field(default_factory=dict) # per-bundle options
|
|
174
|
+
supervisor: SupervisorConfig | None = None
|
|
175
|
+
team_supervisor: TeamSupervisorConfig | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class VerificationConfig:
|
|
180
|
+
"""Verification policy configuration (Ralph-Wiggum pattern).
|
|
181
|
+
|
|
182
|
+
Verification runs at the Stop boundary and can block exit until
|
|
183
|
+
the assistant produces a completion signal.
|
|
184
|
+
|
|
185
|
+
Fields:
|
|
186
|
+
type: Verification type.
|
|
187
|
+
- "completion_promise": Check for promise string in last assistant message.
|
|
188
|
+
- "test_suite": Run `uv run pytest` and check exit code.
|
|
189
|
+
promise: (completion_promise only) The exact string that must appear on a
|
|
190
|
+
standalone line in the last assistant message.
|
|
191
|
+
max_iterations: Maximum number of blocked Stop attempts before auto-bypass.
|
|
192
|
+
max_minutes: Maximum minutes from first block before auto-bypass (None = no limit).
|
|
193
|
+
bypass: If True, skip verification entirely (escape hatch).
|
|
194
|
+
on_incomplete: What to do when verification fails:
|
|
195
|
+
- "block": sys.exit(2) with stderr guidance
|
|
196
|
+
- "warn": print warning, allow Stop
|
|
197
|
+
- "allow": skip verification entirely
|
|
198
|
+
re_inject_prompt: Custom message to print to stderr when blocking.
|
|
199
|
+
If None, a default message is used.
|
|
200
|
+
test_timeout_seconds: (test_suite only) Timeout for pytest command in seconds.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
type: str = "completion_promise" # "completion_promise" | "test_suite"
|
|
204
|
+
promise: str | None = None
|
|
205
|
+
max_iterations: int = 50
|
|
206
|
+
max_minutes: int | None = None
|
|
207
|
+
bypass: bool = False
|
|
208
|
+
on_incomplete: str = "block" # "block", "warn", "allow"
|
|
209
|
+
re_inject_prompt: str | None = None
|
|
210
|
+
test_timeout_seconds: int = 300 # 5 minutes default (test_suite only)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class SessionIntent:
|
|
215
|
+
"""What Forge intends for this session.
|
|
216
|
+
|
|
217
|
+
NOTE: Proxy-owned routing and LLM hyperparameters are intentionally excluded
|
|
218
|
+
from the session schema. Sessions may express only session-owned intent.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
agent: str = "claude-code"
|
|
222
|
+
proxy: ProxyIntent | None = None
|
|
223
|
+
subprocess_proxy: str | None = None # proxy_id for routing subprocesses (supervisor, panel, etc.)
|
|
224
|
+
launch: LaunchIntent | None = None
|
|
225
|
+
system_prompt: SystemPromptIntent | None = None
|
|
226
|
+
memory: MemoryIntent | None = None
|
|
227
|
+
policy: PolicyIntent | None = None
|
|
228
|
+
verification: VerificationConfig | None = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --- Confirmed section - what Claude Code actually did (filled by hooks) ---
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class PolicyConfirmed:
|
|
236
|
+
"""Hook-owned policy state persisted across hook invocations.
|
|
237
|
+
|
|
238
|
+
Since hooks are short-lived processes, stateful policy data must be
|
|
239
|
+
persisted to the session manifest between invocations.
|
|
240
|
+
|
|
241
|
+
Fields:
|
|
242
|
+
forge_version: Version for provenance tracking
|
|
243
|
+
bundles: Active bundle names at last evaluation
|
|
244
|
+
rules_active: Active rule IDs at last evaluation
|
|
245
|
+
decisions: Log of recent policy decisions (bounded to MAX_DECISION_LOG)
|
|
246
|
+
policy_states: Generic per-policy state dict keyed by policy_id
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
forge_version: str | None = None
|
|
250
|
+
bundles: list[str] = field(default_factory=list)
|
|
251
|
+
rules_active: list[str] = field(default_factory=list)
|
|
252
|
+
decisions: list[dict[str, Any]] = field(default_factory=list)
|
|
253
|
+
policy_states: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class VerificationConfirmed:
|
|
258
|
+
"""Hook-owned verification state persisted across Stop invocations.
|
|
259
|
+
|
|
260
|
+
Tracks runtime verification state for the Ralph-Wiggum feedback loop.
|
|
261
|
+
|
|
262
|
+
Fields:
|
|
263
|
+
started_at: ISO8601 timestamp of first blocked Stop (for max_minutes).
|
|
264
|
+
iterations: Number of times Stop was blocked (not total Stop invocations).
|
|
265
|
+
last_result: Outcome of last verification check:
|
|
266
|
+
- "passed": promise found, Stop allowed
|
|
267
|
+
- "failed": promise not found, Stop blocked
|
|
268
|
+
- "warned": promise not found, Stop allowed (on_incomplete=warn)
|
|
269
|
+
- "max_iterations": limit exceeded, auto-bypassed
|
|
270
|
+
- "max_minutes": time limit exceeded, auto-bypassed
|
|
271
|
+
- "bypassed": manually bypassed via %cancel-verification
|
|
272
|
+
- "error": verification check failed due to internal error
|
|
273
|
+
last_error: Short description of last failure (for debugging).
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
started_at: str | None = None
|
|
277
|
+
iterations: int = 0
|
|
278
|
+
last_result: str | None = None
|
|
279
|
+
last_error: str | None = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class CompactionConfirmed:
|
|
284
|
+
"""Compaction tracking state persisted across hook invocations.
|
|
285
|
+
|
|
286
|
+
Records compaction events and pre-compact transcript snapshots for
|
|
287
|
+
session metadata, search indexing, and transcript lineage.
|
|
288
|
+
PreCompact captures the full transcript before compaction; PostCompact
|
|
289
|
+
records the completion timestamp.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
compact_count: int = 0
|
|
293
|
+
last_compact_at: str | None = None # ISO8601, set by PostCompact
|
|
294
|
+
last_compact_type: str | None = None # "auto" | "manual" | "unknown"
|
|
295
|
+
transcript_snapshots: list[dict[str, Any]] = field(default_factory=list)
|
|
296
|
+
# Each entry: {captured_at, reason, source_path, snapshot_path, copied}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass
|
|
300
|
+
class SubagentConfirmed:
|
|
301
|
+
"""Subagent activity tracking persisted across hook invocations.
|
|
302
|
+
|
|
303
|
+
Records subagent stop events for session observability and future
|
|
304
|
+
policy enforcement. Currently observe-only (no blocking).
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
total_count: int = 0
|
|
308
|
+
by_type: dict[str, int] = field(default_factory=dict) # {"Explore": 2, "Bash": 1}
|
|
309
|
+
last_agent_id: str | None = None
|
|
310
|
+
last_agent_type: str | None = None
|
|
311
|
+
last_stop_at: str | None = None # ISO8601
|
|
312
|
+
last_transcript_path: str | None = None # Agent-specific transcript
|
|
313
|
+
last_message_preview: str | None = None # Truncated last_assistant_message (~200 chars)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@dataclass
|
|
317
|
+
class StartedWithProxy:
|
|
318
|
+
"""Proxy identity snapshot captured at session start.
|
|
319
|
+
|
|
320
|
+
This is hook-owned runtime truth for UX/traceability only.
|
|
321
|
+
The proxy remains the authoritative source of routing behavior.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
base_url: str
|
|
325
|
+
proxy_id: str | None = None
|
|
326
|
+
template: str | None = None
|
|
327
|
+
port: int | None = None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass
|
|
331
|
+
class Derivation:
|
|
332
|
+
"""Context derivation tracking for resumed or forked sessions.
|
|
333
|
+
|
|
334
|
+
Records how this session was derived from its parent(s), enabling
|
|
335
|
+
audit trails and context reconstruction. This is CLI-owned (written
|
|
336
|
+
by `forge session resume` and `forge session fork`), not hook-owned.
|
|
337
|
+
|
|
338
|
+
Fields:
|
|
339
|
+
parent_session: Parent session name (same as SessionState.parent_session).
|
|
340
|
+
parent_transcript: Repo-relative path to parent's transcript artifact.
|
|
341
|
+
inherited_proxy: Template from parent's started_with_proxy (if any).
|
|
342
|
+
resume_mode: "native" (--resume --fork-session) or "handoff" (assembled context).
|
|
343
|
+
None = legacy (handoff). Authoritative field for how context was transferred.
|
|
344
|
+
strategy: Context assembly strategy (minimal|structured|full|ai-curated).
|
|
345
|
+
Only set when resume_mode is "handoff" (or legacy None). Null for native resumes.
|
|
346
|
+
depth: How many ancestors were traversed (1 = parent only).
|
|
347
|
+
resumed_at: ISO8601 timestamp when resume was executed.
|
|
348
|
+
lineage: Ancestry chain from parent to oldest ancestor traversed.
|
|
349
|
+
context_file: Repo-relative path to generated context file.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
parent_session: str
|
|
353
|
+
parent_transcript: str | None = None
|
|
354
|
+
inherited_proxy: str | None = None
|
|
355
|
+
resume_mode: str | None = None
|
|
356
|
+
strategy: str | None = "structured"
|
|
357
|
+
depth: int = 1
|
|
358
|
+
resumed_at: str | None = None
|
|
359
|
+
lineage: list[str] = field(default_factory=list)
|
|
360
|
+
context_file: str | None = None
|
|
361
|
+
# Project identity fields for cross-project resume (see design.md §3)
|
|
362
|
+
parent_forge_root: str | None = None # Where to find parent artifacts
|
|
363
|
+
parent_project_root: str | None = None # Must match child's project_root
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class SessionConfirmed:
|
|
368
|
+
"""What Claude Code actually reported via hooks.
|
|
369
|
+
|
|
370
|
+
Ownership: hook-owned runtime facts only (see docs/design.md ownership boundaries).
|
|
371
|
+
|
|
372
|
+
Notes:
|
|
373
|
+
- Paths recorded in `artifacts` are repo-root-relative (e.g., `.forge/artifacts/...`) unless
|
|
374
|
+
otherwise specified.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
claude_session_id: str | None = None # Pre-seeded at launch when possible; validated by SessionStart hook
|
|
378
|
+
transcript_path: str | None = None
|
|
379
|
+
|
|
380
|
+
# Proxy identity snapshot (optional; only set in proxy mode)
|
|
381
|
+
started_with_proxy: StartedWithProxy | None = None
|
|
382
|
+
|
|
383
|
+
# Plan tracking
|
|
384
|
+
latest_plan_path: str | None = None # Worktree-relative path to the latest plan file (e.g., ".claude/plans/x.md")
|
|
385
|
+
|
|
386
|
+
# Session artifacts captured by hooks (repo-root-relative paths)
|
|
387
|
+
artifacts: dict[str, Any] = field(default_factory=dict)
|
|
388
|
+
|
|
389
|
+
# Policy enforcement state (decisions log, per-policy states)
|
|
390
|
+
policy: PolicyConfirmed | None = None
|
|
391
|
+
|
|
392
|
+
# Verification state (iterations, timing for Ralph-Wiggum feedback loop)
|
|
393
|
+
verification: VerificationConfirmed | None = None
|
|
394
|
+
|
|
395
|
+
# Compaction tracking (transcript snapshots, event count)
|
|
396
|
+
compaction: CompactionConfirmed | None = None
|
|
397
|
+
|
|
398
|
+
# Subagent activity tracking (counts, last agent info)
|
|
399
|
+
subagents: SubagentConfirmed | None = None
|
|
400
|
+
|
|
401
|
+
# Sidecar execution mode (proxy bundled in Docker container)
|
|
402
|
+
is_sandboxed: bool = False
|
|
403
|
+
|
|
404
|
+
# Context derivation tracking (for resumed or forked sessions)
|
|
405
|
+
derivation: Derivation | None = None
|
|
406
|
+
|
|
407
|
+
# The exact CWD Claude Code was launched from. Set at launch time by the
|
|
408
|
+
# CLI (not the hook) because the hook runs inside the Claude process which
|
|
409
|
+
# already inherited the CWD. Used by resume to match Claude's project
|
|
410
|
+
# namespace (~/.claude/projects/<encoded-cwd>/).
|
|
411
|
+
claude_project_root: str | None = None
|
|
412
|
+
|
|
413
|
+
confirmed_at: str | None = None # ISO8601 string
|
|
414
|
+
confirmed_by: str | None = None # e.g., "hook:SessionStart"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# --- Main session state structure ---
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@dataclass
|
|
421
|
+
class SessionState:
|
|
422
|
+
"""Complete session state stored in .forge/sessions/<name>/forge.session.json.
|
|
423
|
+
|
|
424
|
+
Schema is intentionally strict:
|
|
425
|
+
- No unknown top-level fields
|
|
426
|
+
- No unknown nested fields
|
|
427
|
+
- No unknown override keys
|
|
428
|
+
|
|
429
|
+
This keeps the file a clear contract rather than an unbounded blob.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
schema_version: int
|
|
433
|
+
name: str
|
|
434
|
+
created_at: str # ISO8601 string
|
|
435
|
+
last_accessed_at: str # ISO8601 string
|
|
436
|
+
parent_session: str | None = None
|
|
437
|
+
is_fork: bool = False
|
|
438
|
+
is_incognito: bool = False
|
|
439
|
+
worktree: Worktree | None = None
|
|
440
|
+
intent: SessionIntent = field(default_factory=SessionIntent)
|
|
441
|
+
# Sparse overrides - same shape as intent, only changed fields present
|
|
442
|
+
overrides: dict[str, Any] = field(default_factory=dict)
|
|
443
|
+
confirmed: SessionConfirmed = field(default_factory=SessionConfirmed)
|
|
444
|
+
# Project identity (see design.md §3). Optional for backward compat with existing manifests.
|
|
445
|
+
forge_root: str | None = None # Forge project root (where .forge/ lives)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# --- Index structures (for ~/.forge/sessions/index.json) ---
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@dataclass
|
|
452
|
+
class SessionIndexEntry:
|
|
453
|
+
"""A single entry in the session index.
|
|
454
|
+
|
|
455
|
+
UUID fields enable fast reverse lookup (find session by UUID) without
|
|
456
|
+
scanning all manifests. These are lazily synced by CLI commands.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
worktree_path: str # Absolute path to worktree (legacy; prefer forge_root)
|
|
460
|
+
project_root: str # Absolute path to main repo (logical repo identity)
|
|
461
|
+
last_accessed_at: str # ISO8601 string
|
|
462
|
+
is_fork: bool = False
|
|
463
|
+
is_incognito: bool = False
|
|
464
|
+
parent_session: str | None = None
|
|
465
|
+
# UUID field for reverse lookup (set by SessionStart hook)
|
|
466
|
+
claude_session_id: str | None = None
|
|
467
|
+
# Empty string (not None) because strict dacite requires str type match;
|
|
468
|
+
# use entry.root for the resolved path (prefers forge_root, falls back to worktree_path).
|
|
469
|
+
forge_root: str = "" # Forge project root (where .forge/ lives)
|
|
470
|
+
checkout_root: str = "" # Git checkout root (--show-toplevel)
|
|
471
|
+
relative_path: str = "." # forge_root relative to checkout_root
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def root(self) -> str:
|
|
475
|
+
"""Resolved project root: forge_root if set, else worktree_path (pre-identity-model fallback)."""
|
|
476
|
+
return self.forge_root or self.worktree_path
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@dataclass
|
|
480
|
+
class SessionIndex:
|
|
481
|
+
"""Global session index for fast listing."""
|
|
482
|
+
|
|
483
|
+
version: int = INDEX_VERSION
|
|
484
|
+
sessions: dict[str, SessionIndexEntry] = field(default_factory=dict)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# --- Factory functions ---
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def create_session_state(
|
|
491
|
+
name: str,
|
|
492
|
+
*,
|
|
493
|
+
proxy_template: str | None = None,
|
|
494
|
+
proxy_base_url: str | None = None,
|
|
495
|
+
parent_session: str | None = None,
|
|
496
|
+
is_fork: bool = False,
|
|
497
|
+
is_incognito: bool = False,
|
|
498
|
+
worktree_path: str | None = None,
|
|
499
|
+
worktree_branch: str | None = None,
|
|
500
|
+
launch_mode: str = LAUNCH_MODE_HOST,
|
|
501
|
+
sidecar_mounts: list[str] | None = None,
|
|
502
|
+
sidecar_image: str | None = None,
|
|
503
|
+
direct_model: str | None = None,
|
|
504
|
+
) -> SessionState:
|
|
505
|
+
"""Create a new session state with defaults.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
name: Session name (must be validated separately).
|
|
509
|
+
proxy_template: Proxy template (e.g., "litellm-gemini"). Optional in direct mode.
|
|
510
|
+
proxy_base_url: Proxy base URL (e.g., "http://localhost:8084"). Optional in direct mode.
|
|
511
|
+
parent_session: Parent session name (for forks).
|
|
512
|
+
is_fork: Whether this is a forked session.
|
|
513
|
+
is_incognito: Whether this is an incognito session.
|
|
514
|
+
worktree_path: Absolute path to git worktree (if any).
|
|
515
|
+
worktree_branch: Git branch name (defaults to session name).
|
|
516
|
+
launch_mode: How Forge should relaunch this session ("host" or "sidecar").
|
|
517
|
+
sidecar_mounts: Raw sidecar mount specs to persist for relaunch.
|
|
518
|
+
sidecar_image: Optional sidecar image override to persist for relaunch.
|
|
519
|
+
direct_model: Optional Claude Code env-ready direct model pin.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
A new SessionState with timestamps set to now.
|
|
523
|
+
"""
|
|
524
|
+
now = now_iso()
|
|
525
|
+
|
|
526
|
+
if (proxy_template is None) != (proxy_base_url is None):
|
|
527
|
+
raise ValueError("proxy_template and proxy_base_url must be provided together")
|
|
528
|
+
|
|
529
|
+
proxy = None
|
|
530
|
+
if proxy_template is not None and proxy_base_url is not None:
|
|
531
|
+
proxy = ProxyIntent(template=proxy_template, base_url=proxy_base_url)
|
|
532
|
+
|
|
533
|
+
launch = LaunchIntent(mode=launch_mode, direct_model=direct_model)
|
|
534
|
+
if launch_mode == LAUNCH_MODE_SIDECAR or sidecar_mounts or sidecar_image is not None:
|
|
535
|
+
launch.sidecar = SidecarLaunchIntent(
|
|
536
|
+
mounts=list(sidecar_mounts or []),
|
|
537
|
+
image=sidecar_image,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
worktree = None
|
|
541
|
+
if worktree_path:
|
|
542
|
+
worktree = Worktree(
|
|
543
|
+
path=worktree_path,
|
|
544
|
+
branch=worktree_branch or name,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return SessionState(
|
|
548
|
+
schema_version=SCHEMA_VERSION,
|
|
549
|
+
name=name,
|
|
550
|
+
created_at=now,
|
|
551
|
+
last_accessed_at=now,
|
|
552
|
+
parent_session=parent_session,
|
|
553
|
+
is_fork=is_fork,
|
|
554
|
+
is_incognito=is_incognito,
|
|
555
|
+
worktree=worktree,
|
|
556
|
+
intent=SessionIntent(
|
|
557
|
+
proxy=proxy,
|
|
558
|
+
launch=launch,
|
|
559
|
+
),
|
|
560
|
+
confirmed=SessionConfirmed(),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def session_state_to_dict(state: SessionState) -> dict[str, Any]:
|
|
565
|
+
"""Convert SessionState to dict.
|
|
566
|
+
|
|
567
|
+
This should be used instead of dataclasses.asdict() when serializing
|
|
568
|
+
session state to JSON.
|
|
569
|
+
"""
|
|
570
|
+
from dataclasses import asdict
|
|
571
|
+
|
|
572
|
+
return asdict(state)
|