simjecture 0.1.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.
- conjecture_solver/__init__.py +10 -0
- conjecture_solver/__main__.py +4 -0
- conjecture_solver/action_handlers.py +459 -0
- conjecture_solver/adapters/__init__.py +7 -0
- conjecture_solver/adapters/base.py +96 -0
- conjecture_solver/adapters/fake.py +155 -0
- conjecture_solver/adapters/pic.py +196 -0
- conjecture_solver/adapters/warpx.py +1375 -0
- conjecture_solver/autonomous_research.py +1071 -0
- conjecture_solver/benchmarks/__init__.py +6 -0
- conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
- conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
- conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
- conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
- conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
- conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
- conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
- conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
- conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
- conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
- conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
- conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
- conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
- conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
- conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
- conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
- conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
- conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
- conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
- conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
- conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
- conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
- conjecture_solver/campaign.py +406 -0
- conjecture_solver/cli.py +963 -0
- conjecture_solver/confirmation.py +287 -0
- conjecture_solver/control.py +234 -0
- conjecture_solver/deployment.py +874 -0
- conjecture_solver/discovery.py +68 -0
- conjecture_solver/domains/__init__.py +40 -0
- conjecture_solver/domains/base.py +157 -0
- conjecture_solver/domains/kinetic_sufficiency.py +521 -0
- conjecture_solver/ledger.py +238 -0
- conjecture_solver/lifecycle.py +131 -0
- conjecture_solver/literature.py +494 -0
- conjecture_solver/llm.py +228 -0
- conjecture_solver/models.py +279 -0
- conjecture_solver/mvp_agent.py +3212 -0
- conjecture_solver/mvp_claims.py +1279 -0
- conjecture_solver/mvp_control.py +322 -0
- conjecture_solver/mvp_guidance.py +172 -0
- conjecture_solver/mvp_launch.py +1296 -0
- conjecture_solver/mvp_monitor.py +1487 -0
- conjecture_solver/mvp_skills.py +538 -0
- conjecture_solver/orchestration.py +578 -0
- conjecture_solver/outbox.py +526 -0
- conjecture_solver/parameters.py +394 -0
- conjecture_solver/proposals.py +427 -0
- conjecture_solver/research_tools.py +278 -0
- conjecture_solver/schema_export.py +246 -0
- conjecture_solver/search.py +1047 -0
- conjecture_solver/semantics.py +146 -0
- conjecture_solver/tui/__init__.py +7 -0
- conjecture_solver/tui/app.py +202 -0
- conjecture_solver/tui/claim_views.py +213 -0
- conjecture_solver/tui/screens.py +1014 -0
- conjecture_solver/warpx_analysis.py +184 -0
- conjecture_solver/warpx_campaign.py +148 -0
- conjecture_solver/warpx_confirmation.py +617 -0
- simjecture-0.1.0.dist-info/METADATA +400 -0
- simjecture-0.1.0.dist-info/RECORD +75 -0
- simjecture-0.1.0.dist-info/WHEEL +4 -0
- simjecture-0.1.0.dist-info/entry_points.txt +4 -0
- simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
- simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Append-only event ledger used by the deterministic development slice."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import sqlite3
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class StoredEvent:
|
|
17
|
+
sequence: int
|
|
18
|
+
campaign_id: str
|
|
19
|
+
event_id: str
|
|
20
|
+
event_type: str
|
|
21
|
+
aggregate_type: str
|
|
22
|
+
aggregate_id: str
|
|
23
|
+
idempotency_key: str | None
|
|
24
|
+
payload: dict[str, Any]
|
|
25
|
+
created_at: str
|
|
26
|
+
previous_hash: str
|
|
27
|
+
event_hash: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class AppendResult:
|
|
32
|
+
event: StoredEvent
|
|
33
|
+
inserted: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IdempotencyConflict(ValueError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SQLiteEventLedger:
|
|
41
|
+
"""SQLite test implementation.
|
|
42
|
+
|
|
43
|
+
Production will implement the same contract in PostgreSQL.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, path: str | Path = ":memory:") -> None:
|
|
47
|
+
self._connection = sqlite3.connect(str(path))
|
|
48
|
+
self._connection.row_factory = sqlite3.Row
|
|
49
|
+
self._connection.execute("PRAGMA foreign_keys = ON")
|
|
50
|
+
self._connection.execute("PRAGMA journal_mode = WAL")
|
|
51
|
+
self._connection.execute(
|
|
52
|
+
"""
|
|
53
|
+
CREATE TABLE IF NOT EXISTS campaign_events (
|
|
54
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
campaign_id TEXT NOT NULL,
|
|
56
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
57
|
+
event_type TEXT NOT NULL,
|
|
58
|
+
aggregate_type TEXT NOT NULL,
|
|
59
|
+
aggregate_id TEXT NOT NULL,
|
|
60
|
+
idempotency_key TEXT,
|
|
61
|
+
payload_json TEXT NOT NULL,
|
|
62
|
+
created_at TEXT NOT NULL,
|
|
63
|
+
previous_hash TEXT NOT NULL,
|
|
64
|
+
event_hash TEXT NOT NULL UNIQUE,
|
|
65
|
+
UNIQUE(campaign_id, idempotency_key)
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
)
|
|
69
|
+
self._connection.commit()
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
self._connection.close()
|
|
73
|
+
|
|
74
|
+
def __enter__(self) -> SQLiteEventLedger:
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def __exit__(self, *_: object) -> None:
|
|
78
|
+
self.close()
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _from_row(row: sqlite3.Row) -> StoredEvent:
|
|
82
|
+
return StoredEvent(
|
|
83
|
+
sequence=row["sequence"],
|
|
84
|
+
campaign_id=row["campaign_id"],
|
|
85
|
+
event_id=row["event_id"],
|
|
86
|
+
event_type=row["event_type"],
|
|
87
|
+
aggregate_type=row["aggregate_type"],
|
|
88
|
+
aggregate_id=row["aggregate_id"],
|
|
89
|
+
idempotency_key=row["idempotency_key"],
|
|
90
|
+
payload=json.loads(row["payload_json"]),
|
|
91
|
+
created_at=row["created_at"],
|
|
92
|
+
previous_hash=row["previous_hash"],
|
|
93
|
+
event_hash=row["event_hash"],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _hash_event(
|
|
98
|
+
*,
|
|
99
|
+
campaign_id: str,
|
|
100
|
+
event_id: str,
|
|
101
|
+
event_type: str,
|
|
102
|
+
aggregate_type: str,
|
|
103
|
+
aggregate_id: str,
|
|
104
|
+
idempotency_key: str | None,
|
|
105
|
+
payload_json: str,
|
|
106
|
+
created_at: str,
|
|
107
|
+
previous_hash: str,
|
|
108
|
+
) -> str:
|
|
109
|
+
canonical = json.dumps(
|
|
110
|
+
{
|
|
111
|
+
"aggregate_id": aggregate_id,
|
|
112
|
+
"aggregate_type": aggregate_type,
|
|
113
|
+
"campaign_id": campaign_id,
|
|
114
|
+
"created_at": created_at,
|
|
115
|
+
"event_id": event_id,
|
|
116
|
+
"event_type": event_type,
|
|
117
|
+
"idempotency_key": idempotency_key,
|
|
118
|
+
"payload_json": payload_json,
|
|
119
|
+
"previous_hash": previous_hash,
|
|
120
|
+
},
|
|
121
|
+
sort_keys=True,
|
|
122
|
+
separators=(",", ":"),
|
|
123
|
+
)
|
|
124
|
+
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
125
|
+
|
|
126
|
+
def append(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
campaign_id: str,
|
|
130
|
+
event_type: str,
|
|
131
|
+
aggregate_type: str,
|
|
132
|
+
aggregate_id: str,
|
|
133
|
+
payload: dict[str, Any],
|
|
134
|
+
idempotency_key: str | None = None,
|
|
135
|
+
event_id: str | None = None,
|
|
136
|
+
created_at: datetime | None = None,
|
|
137
|
+
) -> AppendResult:
|
|
138
|
+
payload_json = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
139
|
+
if idempotency_key is not None:
|
|
140
|
+
existing = self._connection.execute(
|
|
141
|
+
"""
|
|
142
|
+
SELECT * FROM campaign_events
|
|
143
|
+
WHERE campaign_id = ? AND idempotency_key = ?
|
|
144
|
+
""",
|
|
145
|
+
(campaign_id, idempotency_key),
|
|
146
|
+
).fetchone()
|
|
147
|
+
if existing is not None:
|
|
148
|
+
stored = self._from_row(existing)
|
|
149
|
+
if (
|
|
150
|
+
stored.event_type != event_type
|
|
151
|
+
or stored.aggregate_type != aggregate_type
|
|
152
|
+
or stored.aggregate_id != aggregate_id
|
|
153
|
+
or stored.payload != payload
|
|
154
|
+
):
|
|
155
|
+
raise IdempotencyConflict(
|
|
156
|
+
"an idempotency key cannot be reused for a different logical event"
|
|
157
|
+
)
|
|
158
|
+
return AppendResult(event=stored, inserted=False)
|
|
159
|
+
|
|
160
|
+
prior = self._connection.execute(
|
|
161
|
+
"""
|
|
162
|
+
SELECT event_hash FROM campaign_events
|
|
163
|
+
WHERE campaign_id = ? ORDER BY sequence DESC LIMIT 1
|
|
164
|
+
""",
|
|
165
|
+
(campaign_id,),
|
|
166
|
+
).fetchone()
|
|
167
|
+
previous_hash = prior["event_hash"] if prior else "GENESIS"
|
|
168
|
+
actual_event_id = event_id or f"event_{uuid4().hex}"
|
|
169
|
+
actual_created_at = (created_at or datetime.now(UTC)).isoformat()
|
|
170
|
+
event_hash = self._hash_event(
|
|
171
|
+
campaign_id=campaign_id,
|
|
172
|
+
event_id=actual_event_id,
|
|
173
|
+
event_type=event_type,
|
|
174
|
+
aggregate_type=aggregate_type,
|
|
175
|
+
aggregate_id=aggregate_id,
|
|
176
|
+
idempotency_key=idempotency_key,
|
|
177
|
+
payload_json=payload_json,
|
|
178
|
+
created_at=actual_created_at,
|
|
179
|
+
previous_hash=previous_hash,
|
|
180
|
+
)
|
|
181
|
+
cursor = self._connection.execute(
|
|
182
|
+
"""
|
|
183
|
+
INSERT INTO campaign_events (
|
|
184
|
+
campaign_id, event_id, event_type, aggregate_type, aggregate_id,
|
|
185
|
+
idempotency_key, payload_json, created_at, previous_hash, event_hash
|
|
186
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
187
|
+
""",
|
|
188
|
+
(
|
|
189
|
+
campaign_id,
|
|
190
|
+
actual_event_id,
|
|
191
|
+
event_type,
|
|
192
|
+
aggregate_type,
|
|
193
|
+
aggregate_id,
|
|
194
|
+
idempotency_key,
|
|
195
|
+
payload_json,
|
|
196
|
+
actual_created_at,
|
|
197
|
+
previous_hash,
|
|
198
|
+
event_hash,
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
self._connection.commit()
|
|
202
|
+
row = self._connection.execute(
|
|
203
|
+
"SELECT * FROM campaign_events WHERE sequence = ?",
|
|
204
|
+
(cursor.lastrowid,),
|
|
205
|
+
).fetchone()
|
|
206
|
+
assert row is not None
|
|
207
|
+
return AppendResult(event=self._from_row(row), inserted=True)
|
|
208
|
+
|
|
209
|
+
def load(self, campaign_id: str, *, after_sequence: int = 0) -> tuple[StoredEvent, ...]:
|
|
210
|
+
rows = self._connection.execute(
|
|
211
|
+
"""
|
|
212
|
+
SELECT * FROM campaign_events
|
|
213
|
+
WHERE campaign_id = ? AND sequence > ?
|
|
214
|
+
ORDER BY sequence
|
|
215
|
+
""",
|
|
216
|
+
(campaign_id, after_sequence),
|
|
217
|
+
).fetchall()
|
|
218
|
+
return tuple(self._from_row(row) for row in rows)
|
|
219
|
+
|
|
220
|
+
def verify_chain(self, campaign_id: str) -> bool:
|
|
221
|
+
previous_hash = "GENESIS"
|
|
222
|
+
for event in self.load(campaign_id):
|
|
223
|
+
payload_json = json.dumps(event.payload, sort_keys=True, separators=(",", ":"))
|
|
224
|
+
expected = self._hash_event(
|
|
225
|
+
campaign_id=event.campaign_id,
|
|
226
|
+
event_id=event.event_id,
|
|
227
|
+
event_type=event.event_type,
|
|
228
|
+
aggregate_type=event.aggregate_type,
|
|
229
|
+
aggregate_id=event.aggregate_id,
|
|
230
|
+
idempotency_key=event.idempotency_key,
|
|
231
|
+
payload_json=payload_json,
|
|
232
|
+
created_at=event.created_at,
|
|
233
|
+
previous_hash=previous_hash,
|
|
234
|
+
)
|
|
235
|
+
if event.previous_hash != previous_hash or event.event_hash != expected:
|
|
236
|
+
return False
|
|
237
|
+
previous_hash = event.event_hash
|
|
238
|
+
return True
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Pure campaign lifecycle transitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .models import CampaignStatus, StrictModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LifecycleEventType(StrEnum):
|
|
14
|
+
REQUEST_PAUSE = "request_pause"
|
|
15
|
+
BEGIN_QUIESCING = "begin_quiescing"
|
|
16
|
+
MARK_PAUSED = "mark_paused"
|
|
17
|
+
REQUEST_RESUME = "request_resume"
|
|
18
|
+
COMMIT_RESUME = "commit_resume"
|
|
19
|
+
DETECT_CRASH = "detect_crash"
|
|
20
|
+
COMPLETE_RECOVERY = "complete_recovery"
|
|
21
|
+
EMERGENCY_STOP = "emergency_stop"
|
|
22
|
+
REQUIRE_HUMAN_REVIEW = "require_human_review"
|
|
23
|
+
TERMINATE = "terminate"
|
|
24
|
+
ATTEMPT_INTERRUPTED = "attempt_interrupted"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LifecycleEvent(StrictModel):
|
|
28
|
+
event_type: LifecycleEventType
|
|
29
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CampaignState(StrictModel):
|
|
33
|
+
campaign_id: str
|
|
34
|
+
status: CampaignStatus = CampaignStatus.ACTIVE
|
|
35
|
+
revision: int = Field(default=0, ge=0)
|
|
36
|
+
interrupted_attempt_ids: tuple[str, ...] = ()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InvalidTransition(ValueError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _require(state: CampaignState, allowed: set[CampaignStatus], event: LifecycleEvent) -> None:
|
|
44
|
+
if state.status not in allowed:
|
|
45
|
+
allowed_names = ", ".join(sorted(item.value for item in allowed))
|
|
46
|
+
raise InvalidTransition(
|
|
47
|
+
f"{event.event_type.value} requires one of [{allowed_names}], got {state.status.value}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def apply_lifecycle_event(state: CampaignState, event: LifecycleEvent) -> CampaignState:
|
|
52
|
+
status = state.status
|
|
53
|
+
interrupted = state.interrupted_attempt_ids
|
|
54
|
+
|
|
55
|
+
match event.event_type:
|
|
56
|
+
case LifecycleEventType.REQUEST_PAUSE:
|
|
57
|
+
_require(state, {CampaignStatus.ACTIVE}, event)
|
|
58
|
+
status = CampaignStatus.PAUSE_REQUESTED
|
|
59
|
+
case LifecycleEventType.BEGIN_QUIESCING:
|
|
60
|
+
_require(state, {CampaignStatus.PAUSE_REQUESTED}, event)
|
|
61
|
+
status = CampaignStatus.QUIESCING
|
|
62
|
+
case LifecycleEventType.MARK_PAUSED:
|
|
63
|
+
_require(state, {CampaignStatus.QUIESCING}, event)
|
|
64
|
+
status = CampaignStatus.PAUSED
|
|
65
|
+
case LifecycleEventType.REQUEST_RESUME:
|
|
66
|
+
_require(state, {CampaignStatus.PAUSED}, event)
|
|
67
|
+
status = CampaignStatus.RESUMING
|
|
68
|
+
case LifecycleEventType.COMMIT_RESUME:
|
|
69
|
+
_require(state, {CampaignStatus.RESUMING}, event)
|
|
70
|
+
status = CampaignStatus.ACTIVE
|
|
71
|
+
case LifecycleEventType.DETECT_CRASH:
|
|
72
|
+
_require(
|
|
73
|
+
state,
|
|
74
|
+
{
|
|
75
|
+
CampaignStatus.ACTIVE,
|
|
76
|
+
CampaignStatus.PAUSE_REQUESTED,
|
|
77
|
+
CampaignStatus.QUIESCING,
|
|
78
|
+
CampaignStatus.RESUMING,
|
|
79
|
+
},
|
|
80
|
+
event,
|
|
81
|
+
)
|
|
82
|
+
status = CampaignStatus.RECOVERING
|
|
83
|
+
case LifecycleEventType.COMPLETE_RECOVERY:
|
|
84
|
+
_require(state, {CampaignStatus.RECOVERING}, event)
|
|
85
|
+
target = event.payload.get("target", CampaignStatus.ACTIVE.value)
|
|
86
|
+
if target not in {CampaignStatus.ACTIVE.value, CampaignStatus.PAUSED.value}:
|
|
87
|
+
raise InvalidTransition("recovery target must be active or paused")
|
|
88
|
+
status = CampaignStatus(target)
|
|
89
|
+
case LifecycleEventType.EMERGENCY_STOP:
|
|
90
|
+
_require(
|
|
91
|
+
state,
|
|
92
|
+
{
|
|
93
|
+
CampaignStatus.ACTIVE,
|
|
94
|
+
CampaignStatus.PAUSE_REQUESTED,
|
|
95
|
+
CampaignStatus.QUIESCING,
|
|
96
|
+
CampaignStatus.RESUMING,
|
|
97
|
+
CampaignStatus.RECOVERING,
|
|
98
|
+
CampaignStatus.PAUSED,
|
|
99
|
+
},
|
|
100
|
+
event,
|
|
101
|
+
)
|
|
102
|
+
status = CampaignStatus.EMERGENCY_STOPPED
|
|
103
|
+
case LifecycleEventType.REQUIRE_HUMAN_REVIEW:
|
|
104
|
+
_require(state, {CampaignStatus.EMERGENCY_STOPPED}, event)
|
|
105
|
+
status = CampaignStatus.HUMAN_REVIEW
|
|
106
|
+
case LifecycleEventType.TERMINATE:
|
|
107
|
+
_require(
|
|
108
|
+
state,
|
|
109
|
+
{
|
|
110
|
+
CampaignStatus.ACTIVE,
|
|
111
|
+
CampaignStatus.PAUSED,
|
|
112
|
+
CampaignStatus.HUMAN_REVIEW,
|
|
113
|
+
CampaignStatus.EMERGENCY_STOPPED,
|
|
114
|
+
},
|
|
115
|
+
event,
|
|
116
|
+
)
|
|
117
|
+
status = CampaignStatus.TERMINATED
|
|
118
|
+
case LifecycleEventType.ATTEMPT_INTERRUPTED:
|
|
119
|
+
attempt_id = event.payload.get("attempt_id")
|
|
120
|
+
if not isinstance(attempt_id, str) or not attempt_id:
|
|
121
|
+
raise InvalidTransition("attempt_interrupted requires attempt_id")
|
|
122
|
+
if attempt_id not in interrupted:
|
|
123
|
+
interrupted = (*interrupted, attempt_id)
|
|
124
|
+
|
|
125
|
+
return state.model_copy(
|
|
126
|
+
update={
|
|
127
|
+
"status": status,
|
|
128
|
+
"revision": state.revision + 1,
|
|
129
|
+
"interrupted_attempt_ids": interrupted,
|
|
130
|
+
}
|
|
131
|
+
)
|