Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ jobs:
working-directory: libs/third-magic
run: pytest tests/unit

- name: Run Integration Tests
working-directory: libs/third-magic
run: pytest tests/integration

mageflow-mcp-unit-tests:
name: Unit Tests (Mageflow MCP)
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,7 @@ frontend/node_modules/
/app/.idea/
/scripts
/frontend/.idea/
.planning/
.planning/
# OS / tooling
.DS_Store
.codex/
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### ✨ Added

- **Cascade TTL to container sub-tasks** (#136): Swarm and chain signatures now declare their sub-tasks as `Reference` foreign keys (`sub_task_refs`) with `CascadeTTL()`, and refresh their TTL on any write. Writing to a container therefore cascades a TTL refresh to all of its sub-tasks, keeping the whole subtree alive together (cascade edge-following requires a real Redis Stack).
- **Container Status / Progress (`mageflow.astatus`)** (#134): Container signatures (swarms and chains) now expose an `astatus()` method returning a structured `ContainerStatus` (total / finished / failed / running / pending, terminal-state percentage, and completion flag). The new `mageflow.astatus(*ids)` loads several containers in a single Redis lookup and returns a `ContainersStatus` with an aggregate `overall_percentage`, raising on missing or non-container ids.
- **Signing Hatchet Workflows (`MageWorkflow`)**: Native Hatchet `Workflow` objects can now be tracked by mageflow's signature lifecycle, enabling status callbacks (success/failure) without wrapping tasks in mageflow decorators.

Expand All @@ -15,6 +16,7 @@

### 🔄 Changed

- **Rapyer bumped to `>=1.3.4`** (#136): all workspaces now require rapyer 1.3.x for `Reference` foreign keys, `CascadeTTL`, and action-scoped `refresh_ttl`. Adapted to 1.3.x changes: instance `apipeline` re-fetches on entry (swarm `add_tasks` uses `ignore_redis_error=True`), and `RedisConfig` is now a pydantic model (`apply_ttl_config` uses `model_copy` instead of `dataclasses.replace`).
- **App Renamed to Mage Voyance**: Product name changed from "Mageflow Viewer" to "Mage Voyance" across the Tauri config, tray tooltip, onboarding, splash screen, and Homebrew cask.

### 🔒 Security
Expand Down
2 changes: 1 addition & 1 deletion libs/mage-voyance/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ license = {text = "MIT"}
requires-python = ">=3.10,<3.14"
dependencies = [
"thirdmagic>=0.0.4,<0.1.0",
"rapyer>=1.2.5,<1.3.0",
"rapyer>=1.3.4,<1.4.0",
"pydantic>=2.0.0,<3.0.0",
"uvicorn>=0.34.0,<1.0.0",
"fastapi>=0.115.0,<1.0.0",
Expand Down
10 changes: 5 additions & 5 deletions libs/mage-voyance/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions libs/mageflow-e2e/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libs/mageflow-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies = [
"pyjwt>=2.12.0", # TODO: remove when mcp updates past pyjwt 2.11.0 — pinned only for CVE-2024-53861
"pydantic>=2.11.0,<3.0.0",
"thirdmagic>=0.0.1",
"rapyer>=1.2.3,<1.3.0",
"rapyer>=1.3.4,<1.4.0",
"httpx>=0.27.1,<1.0.0",
]

Expand Down
10 changes: 5 additions & 5 deletions libs/mageflow-mcp/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions libs/mageflow/mageflow/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dataclasses
from dataclasses import field
from typing import Optional

Expand Down Expand Up @@ -47,6 +46,6 @@ def apply_ttl_config(ttl_config: TTLConfig):
active_ttl = sig_config.active_ttl or ttl_config.active_ttl
done_ttl = sig_config.ttl_when_sign_done or ttl_config.ttl_when_sign_done

sig_type.Meta = dataclasses.replace(sig_type.Meta, ttl=active_ttl)
sig_type.Meta = sig_type.Meta.model_copy(update={"ttl": active_ttl})
if issubclass(sig_type, Signature):
sig_type.SignatureSettings = SignatureConfig(ttl_when_sign_done=done_ttl)
2 changes: 1 addition & 1 deletion libs/mageflow/tests/integration/hatchet/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ def assert_chain_done(
chain_tasks = [task_map[task_id] for task_id in chain_signature.tasks]
assert_tasks_in_order(wf_by_signature, chain_tasks)
output_value = None
for chain_task_id in chain_signature.tasks:
for chain_task_id in chain_signature.task_ids:
input_params = chain_kwargs.copy()
task = task_map[chain_task_id]
if output_value is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async def test__sub_task_is_cancelled__swarm_still_finish(
for i in range(2):
await swarm.aio_run_in_swarm(task1, regular_message, options=trigger_options)
await swarm.close_swarm()
tasks = await TaskSignature.afind(*swarm.tasks)
tasks = await TaskSignature.afind(*swarm.task_ids)
await asyncio.sleep(15)

# Assert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ async def test__swarm_with_swarms_and_chains__sanity(
assert_chain_done(runs, chain, tasks, check_callbacks=False)

# Check kwargs for an inner task was called
signed_task = tasks_map[chain.tasks[-1]]
signed_task = tasks_map[chain.task_ids[-1]]
assert_signature_done(runs, signed_task, field_int=field_int_val)

# Check the first task is called with msg params
first_task = tasks_map[chain.tasks[0]]
first_task = tasks_map[chain.task_ids[0]]
assert_signature_done(runs, first_task, **msg_dump, **main_swarm_kwargs)

# Check error was not called
Expand All @@ -116,9 +116,9 @@ async def test__swarm_with_swarms_and_chains__sanity(
# **main_swarm_kwargs
)
# Assert swarms were called with params
first_task = tasks_map[base_swarm.tasks[0]]
first_task = tasks_map[base_swarm.task_ids[0]]
assert_signature_done(runs, first_task, base_data=test_ctx)
second_task = tasks_map[base_swarm.tasks[1]]
second_task = tasks_map[base_swarm.task_ids[1]]
assert_signature_done(runs, second_task, **msg_dump)

# Check final success was called
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async def test__chain_end_fail_on_remove_task__able_to_delete(
setup.msg.chain_results, setup.lifecycle_manager, setup.logger
)

sub_tasks_exists = await redis_client.exists(*setup.chain_signature.tasks)
sub_tasks_exists = await redis_client.exists(*setup.chain_signature.task_ids)
assert not sub_tasks_exists
await chain_end_task(setup.msg.chain_results, setup.lifecycle_manager, setup.logger)
chain_exists = await redis_client.exists(*setup.chain_signature.key)
Expand Down Expand Up @@ -219,7 +219,7 @@ async def test__chain_error_fail_on_remove_task__able_to_delete(
setup.logger,
)

sub_tasks_exists = await redis_client.exists(*setup.chain_signature.tasks)
sub_tasks_exists = await redis_client.exists(*setup.chain_signature.task_ids)
assert not sub_tasks_exists
await chain_end_task(setup.msg.chain_results, setup.lifecycle_manager, setup.logger)
chain_exists = await redis_client.exists(*setup.chain_signature.key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async def test_two_consecutive_calls_ignore_second_call__no_concurrency_resource
)

reloaded_swarm = await SwarmTaskSignature.aget(swarm_signature.key)
assert reloaded_swarm.tasks_left_to_run == swarm_signature.tasks[-2:]
assert reloaded_swarm.tasks_left_to_run == swarm_signature.task_ids[-2:]
reloaded_publish_state = await PublishState.aget(
swarm_signature.publishing_state_id
)
Expand Down
8 changes: 4 additions & 4 deletions libs/mageflow/tests/unit/test_remove_ttl.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import dataclasses

import pytest
from thirdmagic.consts import REMOVED_TASK_TTL
from thirdmagic.signature import Signature
Expand Down Expand Up @@ -29,9 +27,11 @@ def _apply_ttl():
)
)
yield
PublishState.Meta = dataclasses.replace(PublishState.Meta, ttl=original_publish_ttl)
PublishState.Meta = PublishState.Meta.model_copy(
update={"ttl": original_publish_ttl}
)
for cls, orig_ttl, orig_settings in originals:
cls.Meta = dataclasses.replace(cls.Meta, ttl=orig_ttl)
cls.Meta = cls.Meta.model_copy(update={"ttl": orig_ttl})
cls.SignatureSettings = orig_settings


Expand Down
6 changes: 2 additions & 4 deletions libs/mageflow/tests/unit/test_ttl_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import dataclasses

import pytest
from thirdmagic.chain import ChainTaskSignature
from thirdmagic.consts import REMOVED_TASK_TTL
Expand All @@ -22,7 +20,7 @@ def _restore_signature_class_vars():
originals = [(cls, cls.Meta.ttl, cls.SignatureSettings) for cls in classes]
yield
for cls, orig_ttl, orig_settings in originals:
cls.Meta = dataclasses.replace(cls.Meta, ttl=orig_ttl)
cls.Meta = cls.Meta.model_copy(update={"ttl": orig_ttl})
cls.SignatureSettings = orig_settings


Expand Down Expand Up @@ -61,7 +59,7 @@ def test_publish_state_follows_swarm_config():
assert PublishState.Meta.ttl == 777


def test_dataclasses_replace_preserves_other_fields():
def test_apply_ttl_config_preserves_other_fields():
original_refresh = TaskSignature.Meta.refresh_ttl
apply_ttl_config(
TTLConfig(
Expand Down
12 changes: 6 additions & 6 deletions libs/mageflow/tests/unit/workflows/test_workflow_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async def test_workflow_in_chain_creates_correct_subtasks(workflow, hatchet_adap
assert len(chain_sig.tasks) == 2

# First sub-task should be the workflow-derived TaskSignature
wf_task = await rapyer.afind_one(chain_sig.tasks[0])
wf_task = await rapyer.afind_one(chain_sig.task_ids[0])
assert wf_task is not None
assert isinstance(wf_task, TaskSignature)
assert wf_task.task_name == workflow.name
Expand All @@ -79,7 +79,7 @@ async def test_workflow_in_chain_subtask_has_container_id(workflow, hatchet_adap

chain_sig = await mageflow.achain([workflow, task_sig])

wf_task = await rapyer.afind_one(chain_sig.tasks[0])
wf_task = await rapyer.afind_one(chain_sig.task_ids[0])
assert wf_task is not None
assert wf_task.signature_container_id == chain_sig.key

Expand All @@ -101,8 +101,8 @@ async def test_workflow_in_chain_with_raw_object(workflow, hatchet_adapter):
assert isinstance(chain_sig, ChainTaskSignature)
assert len(chain_sig.tasks) == 2

first_task = await rapyer.afind_one(chain_sig.tasks[0])
second_task = await rapyer.afind_one(chain_sig.tasks[1])
first_task = await rapyer.afind_one(chain_sig.task_ids[0])
second_task = await rapyer.afind_one(chain_sig.task_ids[1])

assert first_task is not None
assert second_task is not None
Expand Down Expand Up @@ -133,7 +133,7 @@ async def test_workflow_in_swarm_tracked_as_single_unit(workflow, hatchet_adapte

assert isinstance(swarm, SwarmTaskSignature)
assert len(swarm.tasks) == 1
assert swarm.tasks[0] == wf_sig.key
assert swarm.task_ids[0] == wf_sig.key


@pytest.mark.asyncio
Expand Down Expand Up @@ -176,7 +176,7 @@ async def test_workflow_raw_object_in_swarm_tracked_as_single_unit(
assert isinstance(swarm, SwarmTaskSignature)
assert len(swarm.tasks) == 1

wf_task = await rapyer.afind_one(swarm.tasks[0])
wf_task = await rapyer.afind_one(swarm.task_ids[0])
assert wf_task is not None
assert isinstance(wf_task, TaskSignature)
assert wf_task.task_name == workflow.name
8 changes: 4 additions & 4 deletions libs/mageflow/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion libs/third-magic/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ authors = [
license = {text = "MIT"}
requires-python = ">=3.10,<3.14"
dependencies = [
"rapyer>=1.2.5,<1.3.0",
"rapyer>=1.3.4,<1.4.0",
"pydantic>=2.0.0,<3.0.0",
]
[project.optional-dependencies]
Expand All @@ -20,6 +20,7 @@ dev = [
"hatchet-sdk>=1.23.0",
"black>=26.1.0",
"ruff>=0.15.5",
"testcontainers[redis]>=4.14.0,<5.0.0",
]

[build-system]
Expand Down
Empty file.
Loading
Loading