Skip to content

SWF v41 baseline: episodes engine - #45

Merged
wenaus merged 7 commits into
mainfrom
infra/baseline-v41
Aug 7, 2026
Merged

SWF v41 baseline: episodes engine#45
wenaus merged 7 commits into
mainfrom
infra/baseline-v41

Conversation

@wenaus

@wenaus wenaus commented Aug 7, 2026

Copy link
Copy Markdown
Member

Baseline PR for the v41 cycle. The episodes module: a generic episode-building engine with definition hooks for start and end times, builder adoption, ingest reads, arrival stamps, and single reporting per participant. send_message raises on unrecoverable failure instead of swallowing it.

Canonical release record: RELEASE_NOTES.md v41 entry in swf-testbed PR BNLNPPS/swf-testbed#69
Companion: swf-monitor PR BNLNPPS/swf-monitor#46

🤖 Generated with Claude Code

wenaus and others added 6 commits July 30, 2026 10:28
EpisodeDefinition (the per-workflow contract: message-to-event
mapping, participant recognition, end signal, completion pass),
EpisodeBuilder (routes bus traffic to armed definitions and drives
open, append, completion, close per execution; never raises into the
listening agent), and MonitorEpisodeIngest (REST client for the
monitor episode ingest endpoints). A workflow gains an episode
record by implementing one definition; per snapper-ai
docs/EPISODES.md and swf-testbed docs/agentic-workflow-view.md.
A per-episode seen set keeps steady message traffic from re-upserting
its sender on every message; death reports pass through.
started_at and ended_at hooks let a definition supply normalized
message times in place of arrival times — the close carries the
recorded end, which backfilled episodes require.
adopt_open_episodes resumes the builder identity's open episodes at
startup — an episode whose end signal already passed is driven to
completion and close, one still mid-flight keeps appending — so a
builder restart never orphans a live record. The ingest client gains
the episode list and detail reads this needs, and every handled
message carries an arrival stamp as the fallback event time for
writers that stamp nothing.
A send that fails now marks the connection down, attempts one reconnect
and resend regardless of error type (the old error-string filter missed
NotConnectedException), and raises if the message is still unsent. The
silent swallow let a dying agent's workflow run to a false 'completed'
while its end_run evaporated, abandoning RunState 102780 (2026-07-30)
as non-terminal — the System page stale-state warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the v41 “episodes” baseline by adding a workflow-agnostic episode-building engine (definitions, builder, and monitor ingest client), and updates agent/background-execution documentation plus MQ send semantics.

Changes:

  • Added episodes.py implementing EpisodeDefinition, EpisodeBuilder, EpisodeContext, and MonitorEpisodeIngest for swf-monitor episode ingest.
  • Updated BaseAgent.send_message to retry once on any send failure and raise on unrecoverable failure.
  • Expanded concurrency migration guidance for run_in_background in both code docs and README.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/swf_common_lib/episodes.py Adds the core episodes engine and monitor ingest client used by workflow-specific episode definitions/builders.
src/swf_common_lib/base_agent.py Documents background worker concurrency semantics and changes send_message to raise after failed reconnect+retry.
README.md Adds detailed “safe migration” guidance for background-worker concurrency and locking patterns.
Suppressed comments (2)

src/swf_common_lib/episodes.py:105

  • _get() returns response.json() directly; a non-JSON response will raise ValueError and bypass the EpisodeIngestError handling, potentially escaping out of EpisodeBuilder methods. Catch JSON decode errors and rethrow as EpisodeIngestError.
        return response.json()

src/swf_common_lib/episodes.py:298

  • participants_from_message() is allowed to return entries without an id, but handle_message() will add None to seen_participants and may incorrectly suppress future upserts. Filter out entries missing id and only add non-empty ids to seen_participants.
            participants = [
                entry for entry in definition.participants_from_message(message)
                if not (entry.get("id") in context.seen_participants
                        and "died_at" not in entry)
            ]

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +69
if response.status_code >= 400:
raise EpisodeIngestError(
f"POST {url} returned {response.status_code}: "
f"{response.text[:500]}"
)
return response.json()
Comment on lines +312 to +315
except EpisodeIngestError as exc:
logger.error("episode ingest failed for %s: %s",
execution_id, exc)
return False
Comment on lines +337 to +347
try:
self.ingest.close(
scope=definition.scope,
episode_id=execution_id,
ended_at=context.ended_at or context.end_seen_at,
summary=definition.summary(context),
)
except EpisodeIngestError as exc:
logger.error("episode close failed for %s: %s",
execution_id, exc)
del self.active[execution_id]
Comment on lines +254 to +258
for event in record.get("events", []):
context.seen_participants.add(event.get("participant"))
if definition.is_end({"msg_type": event.get("kind")}):
context.end_seen_at = utc_now_iso()
context.ended_at = event.get("time")
An episode with no end seen has no deadline to pass; the caller's guard
made this unreachable, and the narrowing states it where mypy checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Test Coverage Summary

src/swf_common_lib/api_utils.py          87     87     0%   8-198
src/swf_common_lib/base_agent.py        441    441     0%   5-889
src/swf_common_lib/config_utils.py       30     30     0%   7-102
src/swf_common_lib/episodes.py          173    173     0%   23-354
src/swf_common_lib/logging_utils.py      33      0   100%
src/swf_common_lib/rest_logging.py       80     80     0%   8-157
src/swf_common_lib/rucio_utils.py       139    139     0%   5-343
-------------------------------------------------------------------
TOTAL                                   983    950     3%
============================== 5 passed in 0.74s ===============================

@wenaus
wenaus merged commit 0d3cd67 into main Aug 7, 2026
5 checks passed
wenaus added a commit that referenced this pull request Aug 8, 2026
Copilot review findings on PR #45, verified: ingest responses that
decode as non-JSON now raise EpisodeIngestError instead of escaping
the callers' catch; handle_message() traps definition-hook failures so
one malformed bus message cannot take down the listening agent; tick()
retains an episode for retry when its close fails and falls back to an
empty summary when the summary hook fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants