Implement structured pipeline events and telemetry - #16
Conversation
Introduce structured execution events (ExecEvent) emitted during command and pipeline execution phases: plan, start, stdout, stderr, and exit. Implement sh.observe() to register observe hooks for these events, supporting sync and async handlers. Events include per-line stdout/stderr output, timing metadata, and merged tags from context and runtime. This enables richer telemetry and observability integrations without coupling Cuprum to specific logging or tracing frameworks. Updated pipeline internals and subprocess execution to emit these events, and extended ExecutionContext with tags for event metadata. Added comprehensive tests and user guide documentation. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Reviewer's GuideAdds a structured execution event system (ExecEvent/ExecHook) and observe hooks to Cuprum, wiring them through command and pipeline execution, with per-line stdout/stderr emission, timing, tagging, async-safe observer handling, and updated context/docs/tests/public API surface. Sequence diagram for observe hooks during single command executionsequenceDiagram
actor User
participant Context as CuprumContext
participant CtxAPI as context_module
participant Sh as sh_module
participant Cmd as SafeCmd
participant Obs as _CommandObservation
participant ObsHelpers as _observability_module
participant Hook as ExecHook
User->>CtxAPI: scoped(..., observe_hooks=(hook,))
activate CtxAPI
CtxAPI->>Context: narrow(observe_hooks)
CtxAPI-->>User: _ScopedContext
deactivate CtxAPI
User->>Sh: observe(hook)
activate Sh
Sh->>CtxAPI: observe(hook)
activate CtxAPI
CtxAPI->>Context: with_observe_hook(hook)
CtxAPI-->>Sh: HookRegistration
deactivate CtxAPI
Sh-->>User: HookRegistration
deactivate Sh
User->>Cmd: run(capture, echo, context)
activate Cmd
Cmd->>Context: check_allowed(program)
Cmd->>CtxAPI: current_context()
CtxAPI-->>Cmd: CuprumContext
Cmd->>Cmd: _run_before_hooks()
Cmd-->>Cmd: _ExecutionHooks(before, after, observe)
Cmd->>Obs: create _CommandObservation
Note over Cmd,Obs: Merge ExecutionContext.tags with default tags
Cmd->>Obs: emit("plan", _EventDetails(pid=None))
activate Obs
Obs->>ObsHelpers: _emit_exec_event(hooks, ExecEvent(plan,...), pending_tasks)
activate ObsHelpers
loop for each ExecHook
ObsHelpers->>Hook: hook(ExecEvent(plan,...))
alt async hook
Hook-->>ObsHelpers: awaitable
ObsHelpers->>ObsHelpers: create_task(awaitable)
else sync hook
Hook-->>ObsHelpers: None
end
end
deactivate ObsHelpers
deactivate Obs
Cmd->>Cmd: spawn subprocess
Cmd->>Obs: emit("start", _EventDetails(pid))
activate Obs
Obs->>ObsHelpers: _emit_exec_event(..., ExecEvent(start,...))
deactivate Obs
par stdout stream
Cmd->>Cmd: _consume_stream(..., on_line=stdout_handler)
loop each stdout line
Cmd->>Obs: emit("stdout", _EventDetails(pid, line))
Obs->>ObsHelpers: _emit_exec_event(..., ExecEvent(stdout,...))
end
and stderr stream
Cmd->>Cmd: _consume_stream(..., on_line=stderr_handler)
loop each stderr line
Cmd->>Obs: emit("stderr", _EventDetails(pid, line))
Obs->>ObsHelpers: _emit_exec_event(..., ExecEvent(stderr,...))
end
end
Cmd->>Cmd: wait_for_exit_code()
Cmd-->>Cmd: exit_code, duration_s
Cmd->>Obs: emit("exit", _EventDetails(pid, exit_code, duration_s))
Obs->>ObsHelpers: _emit_exec_event(..., ExecEvent(exit,...))
Cmd-->>User: CommandResult
Cmd->>CtxAPI: _wait_for_exec_hook_tasks(pending_tasks)
activate CtxAPI
CtxAPI->>ObsHelpers: _wait_for_exec_hook_tasks(pending_tasks)
ObsHelpers-->>CtxAPI: all tasks done
deactivate CtxAPI
deactivate Cmd
Sequence diagram for observe hooks during pipeline executionsequenceDiagram
participant User
participant Pipeline as _run_pipeline
participant Context as CuprumContext
participant ObsStage as _StageObservation
participant Spawn as _spawn_pipeline_processes
participant Wait as _wait_for_pipeline
participant ObsHelpers as _observability_module
participant Hook as ExecHook
User->>Pipeline: _run_pipeline(parts, capture, echo, context)
activate Pipeline
Pipeline->>Context: current_context()
loop per stage
Pipeline->>Pipeline: _run_before_hooks(cmd)
Pipeline-->>Pipeline: _ExecutionHooks(before, after, observe)
end
Pipeline->>Pipeline: create _StageObservation for each cmd
Note over Pipeline,ObsStage: Merge ExecutionContext.tags with project and pipeline tags
loop for each _StageObservation
Pipeline->>ObsStage: emit("plan", _EventDetails(pid=None))
ObsStage->>ObsHelpers: _emit_exec_event(..., ExecEvent(plan,...))
loop before_hooks
Pipeline->>Hook: before_hook(cmd)
end
end
Pipeline->>Spawn: _spawn_pipeline_processes(..., observations)
activate Spawn
loop per stage index
Spawn->>Spawn: create_subprocess_exec(argv_with_program,...)
Spawn-->>Spawn: process, pid
Spawn->>ObsStage: emit("start", _EventDetails(pid))
ObsStage->>ObsHelpers: _emit_exec_event(..., ExecEvent(start,...))
Spawn->>Spawn: _create_stage_capture_tasks(process, is_last_stage, observation)
note over Spawn,ObsStage: _consume_stream uses on_line callbacks
par stderr lines
loop stderr lines
Spawn->>ObsStage: emit("stderr", _EventDetails(pid, line))
ObsStage->>ObsHelpers: _emit_exec_event(..., ExecEvent(stderr,...))
end
and last stage stdout lines
loop stdout lines
Spawn->>ObsStage: emit("stdout", _EventDetails(pid, line))
ObsStage->>ObsHelpers: _emit_exec_event(..., ExecEvent(stdout,...))
end
end
end
Spawn-->>Pipeline: processes, stderr_tasks, stdout_task, started_at
deactivate Spawn
Pipeline->>Wait: _wait_for_pipeline(processes, pipe_tasks, cancel_grace, started_at)
activate Wait
Wait->>Wait: _PipelineWaitState.from_processes(processes, started_at)
loop each completed process
Wait->>Wait: record exit_code and ended_at
end
Wait-->>Pipeline: _PipelineWaitResult(exit_codes, failure_index, started_at, ended_at)
deactivate Wait
loop per stage index
Pipeline->>ObsStage: emit("exit", _EventDetails(pid, exit_code, duration_s))
ObsStage->>ObsHelpers: _emit_exec_event(..., ExecEvent(exit,...))
end
Pipeline->>Pipeline: build CommandResult per stage
Pipeline->>Pipeline: _run_pipeline_after_hooks(parts, hooks_by_stage, results)
Pipeline->>ObsHelpers: _wait_for_exec_hook_tasks(pending_tasks)
ObsHelpers-->>Pipeline: all observe hooks finished
Pipeline-->>User: PipelineResult
deactivate Pipeline
Class diagram for structured execution events and observability internalsclassDiagram
direction LR
class ExecEvent {
<<dataclass>>
+ExecPhase phase
+Program program
+tuple~str~ argv
+Path cwd
+Mapping~str,str~ env
+int pid
+float timestamp
+str line
+int exit_code
+float duration_s
+Mapping~str,object~ tags
}
class ExecHook {
<<callable>>
+__call__(ExecEvent) Awaitable~None~ | None
}
class CuprumContext {
<<dataclass>>
+frozenset~Program~ allowlist
+tuple~BeforeHook~ before_hooks
+tuple~AfterHook~ after_hooks
+tuple~ExecHook~ observe_hooks
+bool is_allowed(program)
+check_allowed(program)
+narrow(allowlist,before_hooks,after_hooks,observe_hooks) CuprumContext
+with_before_hook(hook) CuprumContext
+with_after_hook(hook) CuprumContext
+with_observe_hook(hook) CuprumContext
+without_before_hook(hook) CuprumContext
+without_after_hook(hook) CuprumContext
+without_observe_hook(hook) CuprumContext
}
class HookRegistration {
+HookRegistration(hook, hook_type)
+detach()
+__enter__() HookRegistration
+__exit__(exc_type, exc, tb) bool
-object _hook
-str _hook_type
-Token~CuprumContext~ _token
}
class ExecutionContext {
<<dataclass>>
+Mapping~Program,bool~ allowlist
+Mapping~str,str~ env
+Path cwd
+IO~str~ stdout_sink
+IO~str~ stderr_sink
+str encoding
+str errors
+Mapping~str,object~ tags
+float cancel_grace
}
class SafeCmd {
+Program program
+Project project
+tuple~str~ argv
+tuple~str~ argv_with_program
+run(capture, echo, context) CommandResult
+run_sync(capture, echo, context) CommandResult
}
class CommandResult {
<<dataclass>>
+Program program
+tuple~str~ argv
+int exit_code
+int pid
+str stdout
+str stderr
}
class _ExecutionHooks {
<<dataclass>>
+tuple~BeforeHook~ before_hooks
+tuple~AfterHook~ after_hooks
+tuple~ExecHook~ observe_hooks
}
class _CommandObservation {
<<dataclass>>
+SafeCmd cmd
+tuple~ExecHook~ observe_hooks
+Path cwd
+Mapping~str,str~ env_overlay
+Mapping~str,object~ tags
+list~Task~ pending_tasks
+emit(phase, details) void
}
class _StageObservation {
<<dataclass>>
+SafeCmd cmd
+_ExecutionHooks hooks
+Mapping~str,object~ tags
+Path cwd
+Mapping~str,str~ env_overlay
+list~Task~ pending_tasks
+emit(phase, details) void
}
class _EventDetails {
<<dataclass>>
+int pid
+str line
+int exit_code
+float duration_s
}
class _SubprocessExecution {
<<dataclass>>
+SafeCmd cmd
+ExecutionContext ctx
+bool capture
+bool echo
+_CommandObservation observation
}
class _PipelineRunConfig {
<<dataclass>>
+ExecutionContext ctx
+bool capture
+bool echo
+bool capture_or_echo
+_StreamConfig stream_config
+IO~str~ stderr_sink
}
class _PipelineWaitState {
<<dataclass>>
+list~Task~ wait_tasks
+dict~Task,int~ task_to_index
+list~int~ exit_codes
+list~float~ started_at
+list~float~ ended_at
+int failure_index
+from_processes(processes, started_at) _PipelineWaitState
}
class _PipelineWaitResult {
<<dataclass>>
+list~int~ exit_codes
+int failure_index
+list~float~ started_at
+list~float~ ended_at
}
class _StreamConfig {
<<dataclass>>
+bool capture_output
+bool echo_output
+IO~str~ sink
+str encoding
+str errors
}
class _observability_module {
<<module>>
+_freeze_str_mapping(mapping) Mapping~str,str~
+_merge_tags(*tags) Mapping~str,object~
+_emit_exec_event(hooks, event, pending_tasks) void
+_wait_for_exec_hook_tasks(pending_tasks) void
}
class context_module {
<<module>>
+current_context() CuprumContext
+get_context() CuprumContext
+scoped(allowlist,before_hooks,after_hooks,observe_hooks) _ScopedContext
+before(hook) HookRegistration
+after(hook) HookRegistration
+observe(hook) HookRegistration
}
class sh_module {
<<module>>
+observe(hook) HookRegistration
}
ExecHook --> ExecEvent : parameter
CuprumContext o--> ExecHook : observe_hooks
CuprumContext o--> BeforeHook
CuprumContext o--> AfterHook
HookRegistration --> CuprumContext : updates
context_module --> HookRegistration : creates
ExecutionContext --> _StreamConfig : configures
ExecutionContext --> _CommandObservation : provides tags, env, cwd
SafeCmd --> _ExecutionHooks : uses via _run_before_hooks
SafeCmd --> _CommandObservation : constructs
SafeCmd --> _SubprocessExecution : wraps
_ExecutionHooks o--> BeforeHook
_ExecutionHooks o--> AfterHook
_ExecutionHooks o--> ExecHook
_CommandObservation --> ExecHook : notifies
_StageObservation --> ExecHook : notifies
_CommandObservation --> _EventDetails : uses
_StageObservation --> _EventDetails : uses
_SubprocessExecution --> _CommandObservation
_PipelineRunConfig --> ExecutionContext
_PipelineRunConfig --> _StreamConfig
_PipelineWaitState --> _PipelineWaitResult
_observability_module --> ExecHook : calls
_observability_module --> ExecEvent : emits
_observability_module --> _CommandObservation : used by
_observability_module --> _StageObservation : used by
context_module --> CuprumContext
context_module --> ExecHook
sh_module --> ExecEvent
sh_module --> ExecHook
sh_module --> context_module
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughThis pull request introduces a structured execution events system to Cuprum. It adds a new Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Areas requiring extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix: Comment on lines +203 to +225 hooks_by_stage = tuple(_run_before_hooks(cmd) for cmd in parts)
pending_tasks: list[asyncio.Task[None]] = []
cwd = None if config.ctx.cwd is None else Path(config.ctx.cwd)
env_overlay = _freeze_str_mapping(config.ctx.env)
observations = tuple(
_StageObservation(
cmd=cmd,
hooks=hooks,
tags=_merge_tags(
{
"project": cmd.project.name,
"capture": capture,
"echo": echo,
"pipeline_stage_index": idx,
"pipeline_stages": len(parts),
},
config.ctx.tags,
),
cwd=cwd,
env_overlay=env_overlay,
pending_tasks=pending_tasks,
)
for idx, (cmd, hooks) in enumerate(zip(parts, hooks_by_stage, strict=True))❌ New issue: Complex Method |
This comment was marked as resolved.
This comment was marked as resolved.
…e and clarity - Introduce _PipelineStageResultInputs dataclass for encapsulating inputs. - Split _run_pipeline logic into smaller functions: _build_pipeline_observations, _emit_plan_events_and_run_before_hooks, _build_pipeline_stage_results, and _finalize_pipeline_execution. - Streamline async pipeline execution, error handling, and event emission. - Add detailed typing and improve code readability while maintaining behavior. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
|
@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix: Comment on file import asyncio
import dataclasses as dc
import sys
import time❌ New issue: Lines of Code in a Single File |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
Extract core pipeline logic from _pipeline_internals.py into three new modules: - _pipeline_streams.py for stream coordination - _pipeline_wait.py for pipeline waiting and fail-fast handling - _process_lifecycle.py for process lifecycle management This improves code organization and separation of concerns by isolating stream management, process lifecycle, and pipeline wait logic. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Moved pipeline subprocess spawning and stream wiring functionality from _pipeline_internals.py to a new module _pipeline_spawn.py. This change organizes the codebase by separating spawn and cleanup helpers into their own module, maintaining cohesion and avoiding import cycles. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 7
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (20)
cuprum/__init__.py(3 hunks)cuprum/_observability.py(1 hunks)cuprum/_pipeline_internals.py(2 hunks)cuprum/_pipeline_spawn.py(1 hunks)cuprum/_pipeline_streams.py(1 hunks)cuprum/_pipeline_wait.py(1 hunks)cuprum/_process_lifecycle.py(1 hunks)cuprum/_streams.py(4 hunks)cuprum/_testing.py(1 hunks)cuprum/context.py(13 hunks)cuprum/events.py(1 hunks)cuprum/sh.py(6 hunks)cuprum/unittests/test_observe.py(1 hunks)cuprum/unittests/test_pipeline.py(1 hunks)docs/cuprum-design.md(1 hunks)docs/roadmap.md(1 hunks)docs/users-guide.md(1 hunks)tests/behaviour/test_structured_events.py(1 hunks)tests/conftest.py(1 hunks)tests/features/structured_events.feature(1 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: For Python files, ensure testing passes all relevant unit and behavioral tests by runningmake test.
For Python files, ensure linting passes by runningmake lint.
For Python files, ensure formatting adheres to standards by runningmake check-fmtand applyingmake fmtif needed.
For Python files, ensure type checking passes by runningmake typecheck.
For Python development, refer to Python-specific guidelines in the.rules/directory, including Python Code Style Guidelines, Context Managers, Exceptions and Logging, Generators, Project Configuration, Return Patterns, and Typing.
**/*.py: Use snake_case for Python filenames (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix private/internal functions and variables with a single underscore (_)
Enable full static type coverage using Pyright and maintain typing throughout the codebase
Use TypedDict or Dataclass for structured data, preferring @DataClass(slots=True) for internal-only usage
Avoid using Any type; use Unknown, generics, or cast() with documentation instead
Provide explicit return type annotations (e.g., -> None, -> str) for all public functions and class methods
Enforce strict mode in Pyright and treat all Pyright warnings as CI errors; use # pyright: ignore sparingly with explanation
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use .env or settings modules for environment-specific configuration; never hardcode secrets
Use Ruff for formatting; let Ruff handle whitespace and formatting entirely
Use NumPy-format docstrings for public functions, classes, and modules
Use inline comments to explain tricky or non-obvious code logic and decisions
**/*.py: Use context managers (withcontextlib.contextmanageror class-based__enter__/__exit__) to encapsulate setup and teardown logic for resource management (f...
Files:
tests/behaviour/test_structured_events.pytests/conftest.pycuprum/events.pycuprum/sh.pycuprum/_pipeline_streams.pycuprum/unittests/test_pipeline.pycuprum/context.pycuprum/unittests/test_observe.pycuprum/_pipeline_spawn.pycuprum/_streams.pycuprum/_observability.pycuprum/__init__.pycuprum/_process_lifecycle.pycuprum/_pipeline_wait.pycuprum/_pipeline_internals.pycuprum/_testing.py
⚙️ CodeRabbit configuration file
**/*.py: - Keep C90 / mccabe complexity ≤ 9
- Follow single responsibility and CQRS (command/query segregation)
- Prefer structural pattern matching to
- Prefer structural pattern matching over
isinstance()or imperative decomposition.- Docstrings must follow the
numpystyle guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.- Move conditionals with >2 branches to predicate/helper functions
- Avoid
eval,exec,pickle, monkey-patching,ctypes, unsafe shell- Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
- Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
- Lint suppressions:
- Blanket
# noqa, file-level skips, and categories are forbidden- Only narrow in-line disables (
# noqa: XYZ) are permitted, and must be accompanied byFIXME:or a ticket link, and used only as a last resort.- Use
pytestfixtures for shared setup (conftest.pyorfixtures/)- Replace duplicate tests with
@pytest.mark.parametrize- Prefer
pytest-mockorunittest.mockfor stubs/mocks- Use
assert …, "message"over bare asserts- Reflect all API/behaviour changes in
docs/and update roadmap on completion- Files must not exceed 400 logical lines:
- Decompose large modules into subpackages
- Split large
match/caseor dispatch tables by domain and collocate with targets if appropriate- Move bulky data (fixtures, templates) to external files for parsing at runtime
- Mutable defaults and shadowed built-ins are forbidden
- All code must have clear type hints using modern style (
A | B,list[str],class Foo[A]:,type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.- All path manipulation must be performed using pathlib for cross platform safety. Do not use string manipulation for extraction of path components or filename ele...
Files:
tests/behaviour/test_structured_events.pytests/conftest.pycuprum/events.pycuprum/sh.pycuprum/_pipeline_streams.pycuprum/unittests/test_pipeline.pycuprum/context.pycuprum/unittests/test_observe.pycuprum/_pipeline_spawn.pycuprum/_streams.pycuprum/_observability.pycuprum/__init__.pycuprum/_process_lifecycle.pycuprum/_pipeline_wait.pycuprum/_pipeline_internals.pycuprum/_testing.py
**/test_*.py
📄 CodeRabbit inference engine (.rules/python-00.md)
**/test_*.py: Use pytest idioms: prefer fixtures over setup/teardown methods, parametrize broadly, avoid unnecessary mocks
Group related tests using class with method names prefixed by test_
Write tests from a user's perspective; test public behavior rather than internals
Avoid mocking too much; prefer test doubles only for external services or non-deterministic behaviors
Files:
tests/behaviour/test_structured_events.pycuprum/unittests/test_pipeline.pycuprum/unittests/test_observe.py
**/*test*.py
📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)
Use specific exception types and message constraints with
pytest.raises(SpecificError, match=r"pattern")in tests; avoid overly broad exception assertions (B017)
Files:
tests/behaviour/test_structured_events.pytests/conftest.pycuprum/unittests/test_pipeline.pycuprum/unittests/test_observe.pycuprum/_testing.py
docs/**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
docs/**/*.md: Use markdown files within thedocs/directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in thedocs/directory to reflect the latest state.
All documentation must adhere to the documentation style guide atdocs/documentation-style-guide.md.
Record any design decisions made in the relevant design document.
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
**/*.md: For Markdown files (.md only), ensure linting passes by runningmake markdownlint.
For Markdown files, validate Mermaid diagrams by runningmake nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes.
Validate Markdown files usingmake markdownlint.
Runmake fmtafter any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by runningmake nixie.
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
⚙️ CodeRabbit configuration file
**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")
- Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
- Headings must not be wrapped.
- Documents must start with a level 1 heading
- Headings must correctly increase or decrease by no more than one level at a time
- Use GitHub-flavoured Markdown style for footnotes and endnotes.
- Numbered footnotes must be numbered by order of appearance in the document.
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
docs/**/*.{md,mdx,rst,txt}
📄 CodeRabbit inference engine (docs/documentation-style-guide.md)
docs/**/*.{md,mdx,rst,txt}: Use British English based on Oxford English Dictionary (en-GB-oxendict) conventions: use -ize suffixes (realize, organization), -lyse suffixes (analyse, paralyse, catalyse), -our suffixes (colour, behaviour, neighbour), -re suffixes (calibre, centre, fibre), double 'l' (cancelled, counsellor, cruellest), maintain 'e' (likeable, liveable, rateable), -ogue suffixes (analogue, catalogue)
The word 'outwith' is acceptable in documentation
Use the Oxford comma in documentation: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns in documentation, for example 'Lille Industries are expanding'
Write headings in sentence case in documentation
Use Markdown headings (#,##,###, and so on) in order without skipping levels
Always provide a language identifier for fenced code blocks in documentation; use 'plaintext' for non-code text
Use-as the first level bullet and renumber lists when items change in documentation
Prefer inline links using[text](url)or angle brackets around the URL in documentation
Ensure blank lines before and after bulleted lists and fenced blocks in documentation
Ensure tables have a delimiter line below the header row in documentation
Expand any uncommon acronym on first use in documentation, for example 'Continuous Integration (CI)'
Wrap paragraphs at 80 columns in documentation
Wrap code at 120 columns in documentation
Do not wrap tables in documentation
Use footnotes referenced with[^label]in documentation
Include Mermaid diagrams in documentation where they add clarity
When embedding figures in documentation, useand provide brief alt text describing the content
Add a short description before each Mermaid diagram in documentation so screen readers can understand it
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
docs/**/*.{md,mdx,rst,txt,rs}
📄 CodeRabbit inference engine (docs/documentation-style-guide.md)
Keep US spelling when used in API contexts, for example 'color'
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (docs/documentation-style-guide.md)
Follow markdownlint recommendations for Markdown formatting
Files:
docs/cuprum-design.mddocs/roadmap.mddocs/users-guide.md
**/unittests/test_*.py
📄 CodeRabbit inference engine (.rules/python-00.md)
Colocate unit tests with code using an unittests subdirectory with test_ prefix (e.g., user_auth/unittests/test_models.py)
Files:
cuprum/unittests/test_pipeline.pycuprum/unittests/test_observe.py
docs/users-guide.md
📄 CodeRabbit inference engine (AGENTS.md)
docs/users-guide.md: Ensure new functionality is clearly documented in thedocs/users-guide.mdfile.
Ensure revised functionality is clearly documented in thedocs/users-guide.mdfile.
docs/users-guide.md: Addsh.maketo constructSafeCmdinstances with typed argv handling and minimal builder examples; document the expected builder pattern indocs/users-guide.md
Document hook usage patterns indocs/users-guide.md
Provide a scaffold and guidance for project-specific builders, including a template module and checklist indocs/users-guide.md
Files:
docs/users-guide.md
🧬 Code graph analysis (8)
cuprum/sh.py (6)
cuprum/_observability.py (4)
_emit_exec_event(33-43)_freeze_str_mapping(16-21)_merge_tags(24-30)_wait_for_exec_hook_tasks(50-57)cuprum/_pipeline_internals.py (4)
_run_before_hooks(54-62)_run_pipeline(204-273)emit(74-97)_EventDetails(101-105)cuprum/_process_lifecycle.py (1)
_merge_env(103-111)cuprum/context.py (1)
observe(476-491)cuprum/events.py (1)
ExecEvent(23-66)cuprum/_streams.py (2)
_StreamConfig(17-24)_consume_stream(27-36)
cuprum/context.py (3)
cuprum/catalogue.py (1)
allowlist(67-69)cuprum/unittests/test_observe.py (2)
hook(41-42)hook(55-56)tests/behaviour/test_structured_events.py (1)
hook(63-64)
cuprum/_pipeline_spawn.py (1)
cuprum/_process_lifecycle.py (3)
_build_spawn_observations(114-146)_cleanup_spawned_processes(57-81)_spawn_pipeline_processes(149-215)
cuprum/_observability.py (3)
cuprum/events.py (1)
ExecEvent(23-66)cuprum/unittests/test_observe.py (2)
hook(41-42)hook(55-56)tests/behaviour/test_structured_events.py (1)
hook(63-64)
cuprum/__init__.py (2)
cuprum/context.py (7)
HookRegistration(366-423)after(451-473)allow(344-363)before(426-448)current_context(195-197)observe(476-491)scoped(251-287)cuprum/events.py (1)
ExecEvent(23-66)
cuprum/_process_lifecycle.py (4)
cuprum/_observability.py (2)
_freeze_str_mapping(16-21)_merge_tags(24-30)cuprum/_pipeline_streams.py (4)
_collect_pipe_results(181-189)_PipelineRunConfig(18-37)_create_stage_capture_tasks(93-152)capture_or_echo(26-27)cuprum/_pipeline_internals.py (3)
_StageObservation(66-97)_EventDetails(101-105)emit(74-97)cuprum/sh.py (5)
SafeCmd(399-517)_EventDetails(236-240)argv_with_program(413-415)stdout(160-162)emit(209-232)
cuprum/_pipeline_wait.py (2)
cuprum/_pipeline_streams.py (2)
_collect_pipe_results(181-189)_surface_unexpected_pipe_failures(192-204)cuprum/_process_lifecycle.py (2)
_cleanup_pipeline_on_error(84-100)_terminate_pipeline_remaining_stages(232-264)
cuprum/_testing.py (3)
cuprum/_pipeline_streams.py (1)
_prepare_pipeline_config(40-59)cuprum/_pipeline_wait.py (2)
_PipelineWaitResult(21-25)_wait_for_pipeline(89-136)cuprum/_process_lifecycle.py (1)
_merge_env(103-111)
🪛 LanguageTool
docs/cuprum-design.md
[uncategorized] ~932-~932: Possible missing comma found.
Context: ...xecution and awaited before returning results so run_sync() does not leak pending t...
(AI_HYDRA_LEO_MISSING_COMMA)
docs/users-guide.md
[grammar] ~373-~373: The verb ‘observe’ does not usually follow articles like ‘an’. Check that ‘observe’ is spelled correctly; using ‘observe’ as a noun may be non-standard.
Context: ...nts For richer observability, register an observe hook with sh.observe(). Observe hooks...
(A_INFINITIVE)
🔍 Remote MCP
Let me try searching the web for information about Cuprum and its architecture:The web search results are returning information about stock trading patterns (cup and handle) and React hooks, which are not relevant to the Cuprum library. Let me refine my search:Based on my research, I've gathered relevant context about structured events and telemetry patterns in Python. Let me compile the findings for the PR review.
Summary of Additional Context Found
Structured Events and Telemetry Patterns
The PR implements structured execution events following industry best practices found in telemetry libraries:
Event Design Pattern: In OpenTelemetry, events are discrete, time-stamped occurrences in an application's life cycle that provide specific information about what happened at a particular time. These can represent user actions, system state changes, or significant milestones in application processing. Each event typically includes attributes, such as timestamps, severity levels, and additional metadata that describe the event.
Async Hook Handling: If you are using async support, then hooks registered must be async functions, rather than plain functions. The PR's approach to handling both synchronous and asynchronous observers via _emit_exec_event and _await_awaitable aligns with this pattern.
Telemetry-Agnostic Design: Similar to Jupyter's Telemetry approach, telemetry systems enable applications to record events—actions by application users—and transmit them to remote (or local) destinations as structured data. It works with Python's standard logging library to handle the transmission of events allowing users to send events to local files, over the web, etc. Cuprum's design avoids tight coupling to specific telemetry stacks.
Implementation Review Points
-
Event Phases and Metadata: The PR's event phases (plan, start, stdout, stderr, exit) and metadata fields (pid, duration_s, exit_code, tags) align with standard telemetry event models.
-
Per-Line Emission: Structured event data such as the user ID, login status, and authentication method capture information that makes events easy to analyze and correlate with other telemetry data. The implementation's per-line stdout/stderr emission enables granular observability.
-
Complexity Management: The PR acknowledges code complexity through the proposed refactoring of
_run_pipeline(cyclomatic complexity 13) and extraction of pipeline-related logic into dedicated modules (_pipeline_wait.py,_process_lifecycle.py,_pipeline_streams.py), addressing file size and maintainability concerns identified in code reviews. -
Async Task Leak Prevention: The PR's careful handling of pending tasks in
_wait_for_exec_hook_tasksprevents background task leaks, which is critical for long-running async operations.
[::Context7::]
🔇 Additional comments (63)
tests/features/structured_events.feature (1)
1-10: LGTM! Clear BDD feature definition.The feature file clearly describes the structured execution events behavior. The scenario covers the key aspects: output events (stdout/stderr lines) and timing/tag metadata.
docs/roadmap.md (1)
55-56: LGTM! Roadmap accurately reflects completed work.The task description now correctly captures the implemented scope: stdout/stderr line emissions, timings, tag metadata, and
sh.observe()registration.cuprum/unittests/test_pipeline.py (1)
423-423: LGTM! Test correctly updated for new timing parameter.The
started_atparameter addition aligns with the refactored pipeline wait logic that now tracks per-stage start times for timing metadata.tests/behaviour/test_structured_events.py (5)
15-17: LGTM! Proper use of TYPE_CHECKING guard.The
ExecEventimport is correctly guarded withTYPE_CHECKINGto avoid runtime import costs whilst maintaining type hint support.
37-50: LGTM! Test fixture clearly constructs observed command.The fixture builds a Python command that writes to both stdout and stderr, providing comprehensive coverage for observe hook testing.
52-70: LGTM! Observe hook collection pattern is clear.The test correctly demonstrates the observe hook API: registering a hook with
sh.observe(hook)within a scoped allowlist context, and passing custom tags viaExecutionContext.
72-81: LGTM! Output event validation is comprehensive.The test correctly validates that stdout and stderr line events are emitted and contain the expected output lines.
83-98: LGTM! Timing and tag metadata validation is thorough.The test validates all key metadata: process ID, exit code, duration, custom tags (run_id), and default tags (project).
docs/cuprum-design.md (1)
913-933: LGTM! Comprehensive documentation of observe hooks.The section clearly documents the structured event stream design decisions:
- Event phases and pipeline stage tagging
- Per-line emission semantics
- Timing measurement approaches (wall-clock vs monotonic)
- Tag precedence rules
- Async observer task lifecycle management
The documentation aligns with the implementation and provides clear guidance for users.
docs/users-guide.md (1)
371-408: LGTM! Clear user guide section for structured events.The documentation provides a practical example demonstrating:
- Registering observe hooks with
sh.observe()- Collecting
ExecEventvalues- Filtering events by phase (stdout, stderr, exit)
- Passing custom tags via
ExecutionContext.tags- Accessing event metadata (line, tags, etc.)
The example aligns with the behavioral test in
tests/behaviour/test_structured_events.py, ensuring consistency between documentation and implementation.cuprum/__init__.py (2)
34-45: LGTM! Public API exports are correctly added.The new observability types and functions are properly imported and exposed:
ExecHookfromcuprum.contextobservefromcuprum.contextExecEventfromcuprum.eventsThese align with the documented API in the users guide and design documentation.
61-99: LGTM! all list correctly updated.The new exports (
ExecEvent,ExecHook,observe) are correctly added to__all__, maintaining the existing organisational structure of the list.cuprum/_pipeline_spawn.py (1)
1-20: Wrapper module is justified and currently imported by other modules.This module is imported by
cuprum/_pipeline_internals.py, which depends on_spawn_pipeline_processesfrom this wrapper. The original suggestion to remove this module is invalid because callers already reference it. Removing it would break the import in_pipeline_internals.py.The circular import claim in the docstring should be documented further if there is an actual cycle involving
_process_lifecycleand_pipeline_internals. Verify whether the cycle exists and add a brief explanation to the docstring if it does (e.g., which modules would create the cycle and why the wrapper resolves it).Likely an incorrect or invalid review comment.
cuprum/_testing.py (1)
17-24: LGTM—import provenance updated to reflect refactored modules.The import reorganisation correctly reflects the extraction of pipeline internals into dedicated modules (_pipeline_streams, _pipeline_wait, _process_lifecycle). The exported test surface remains unchanged.
tests/conftest.py (1)
31-36: LGTM—minimal validation aligns with pytest-bdd semantics.The
runtestimplementation performs a simple string check for "Feature:" to confirm the file resembles a Gherkin feature. Whilst this won't catch malformed features, it serves the stated purpose of enabling CLI selection without altering pytest-bdd behaviour.cuprum/events.py (1)
19-72: LGTM—well-structured event model for telemetry integration.The event design aligns with industry best practices for structured telemetry. The frozen dataclass with slots ensures immutability and memory efficiency. The ExecHook type alias correctly supports both synchronous and asynchronous observers, enabling flexible integration patterns.
cuprum/unittests/test_observe.py (3)
49-73: LGTM—hook registration lifecycle correctly validated.The test confirms that observe hooks attach/detach cleanly and that detaching halts event collection. The assertions appropriately check hook count before/after registration and event capture behaviour.
76-123: LGTM—comprehensive event payload validation.The test verifies all key event attributes: phase sequence (plan→start→exit), stdout/stderr line emission, timing data (duration_s), process metadata (pid, cwd, env), and tag propagation (project, run_id). The assertions are thorough and appropriate.
126-159: LGTM—pipeline stage tagging validated.The test confirms that per-stage events include pipeline_stage_index tags and that the final stdout reflects combined stage outputs. The assertions correctly verify exit event count and stage-specific stdout emission.
cuprum/_observability.py (1)
33-43: LGTM—async task leak prevention correctly implemented.The emission logic correctly schedules async hooks as background tasks and appends them to
pending_tasksfor later awaiting. This prevents task leaks and ensures hooks complete beforerun_syncreturns.cuprum/_pipeline_streams.py (3)
40-59: LGTM—dynamic import avoids circular dependency.The
_sh_module()dynamic import pattern is appropriate for internal helpers to avoid circular imports whilst still accessing ExecutionContext. The fallback to default context whencontext is Noneensures robustness.
93-152: LGTM—conditional observation callbacks correctly implemented.The creation of
on_linecallbacks only whenobservation.hooks.observe_hooksis non-empty avoids overhead when no observers are registered. The inline function definitions correctly captureobservation,process.pid, and_EventDetailsfor event emission.
192-204: LGTM—expected pipe failures correctly filtered.The filtering of
BrokenPipeErrorandConnectionResetErroras normal termination (e.g., whenheadcloses stdin early) is appropriate. Other exceptions are correctly surfaced to indicate genuine failures.cuprum/_streams.py (2)
67-109: LGTM—incremental decoding correctly implemented.The use of
codecs.getincrementaldecoderensures multibyte characters split across chunk boundaries are decoded correctly. Thedecoder.decode(b"", final=True)call flushes any remaining buffered bytes, and the final partial line handling (line 105) ensures all output is emitted.
201-230: LGTM—line emission logic correctly handles line endings.The
_emit_completed_linesfunction correctly usessplitlines(keepends=True)to preserve line endings, checks the last line for a trailing ending, and pops it as a remainder if incomplete. The_strip_line_endinghelper normalises both\r\nand\n/\rendings. Edge cases (empty text, no newlines) are handled by returning the input as remainder.cuprum/context.py (5)
54-62: LGTM—observe_hooks field correctly added to CuprumContext.The addition of
observe_hookswith FIFO ordering is documented in the docstring (line 55) and correctly initialised as an empty tuple. The design choice of FIFO (vs LIFO for after_hooks) aligns with typical event stream processing patterns.
86-140: LGTM—narrow() correctly propagates observe_hooks.The
narrow()method correctly extendsobserve_hookswith FIFO ordering (line 133) and passes them to the new context (line 139). The docstring (lines 105-106) documents the FIFO semantics.
168-175: LGTM—observe hook mutation methods correctly implemented.The
with_observe_hookmethod appends the hook to maintain FIFO order, andwithout_observe_hookcorrectly filters out the specific hook instance. The implementations mirror the before/after hook patterns.
384-401: LGTM—HookRegistration correctly supports observe hooks.The updated signature accepts
ExecHookin the union type (line 386), thehook_typeLiteral includes "observe" (line 387), and the dispatch logic (lines 399-400) correctly routes observe hooks throughwith_observe_hook. The token-based restoration semantics remain consistent.
476-491: LGTM—observe() public API correctly implemented.The function signature, docstring, and return type align with the existing
before()andafter()patterns. The integration withHookRegistrationensures consistent detach() semantics and context manager support.cuprum/sh.py (9)
18-45: LGTM!Imports are well-organised: runtime helpers from
_observability, type-only imports guarded underTYPE_CHECKING, and explicit re-export ofobserve.
185-198: LGTM!The
tagsfield follows the established pattern for optional context parameters and uses an immutableMappingtype appropriately.
243-259: LGTM!The cancellation handling correctly terminates the process, awaits consumers to prevent resource leaks, and re-raises
CancelledError. Usingtime.perf_counter()for duration measurement is the right choice for monotonic high-resolution timing.
262-268: LGTM!Clean parameter object pattern that bundles execution context and observation together.
271-288: LGTM!Subprocess creation correctly configures pipes based on capture/echo flags and safely handles the optional cwd parameter.
291-354: LGTM!Stream consumption and line-by-line observation emission are correctly wired. The use of
dc.replacefor stderr configuration is clean, and consumer tasks are properly awaited after process exit.
357-395: LGTM!The subprocess lifecycle is well-orchestrated: spawn → start event → stream consumption → exit event with timing. The defensive
max(0.0, ...)for duration is a sensible guard.
450-488: LGTM!The execution flow correctly sequences plan → before_hooks → execute → after_hooks → await pending tasks. Cancellation handling ensures pending async hooks are awaited before propagation.
597-598: LGTM!Public API correctly exports
observefor user access.cuprum/_pipeline_wait.py (4)
37-51: LGTM!The classmethod correctly initialises wait tasks and index mappings for efficient task-to-stage resolution.
54-73: LGTM!Fail-fast logic correctly records the first failure, updates timing, and terminates remaining stages (excluding the final stage which needs no downstream termination).
76-86: LGTM!Correctly defers pipe failure surfacing when an exception is already being propagated, preventing exception masking.
89-136: LGTM with minor observation.The wait loop correctly implements fail-fast semantics using
FIRST_COMPLETED. The finally block's assignment topipe_resultsis unused after the function exits, but this is acceptable since_finalize_pipeline_waitis called for its side effects (surfacing unexpected failures).cuprum/_process_lifecycle.py (10)
1-17: LGTM!Clean import structure with TYPE_CHECKING guards preventing circular imports.
19-29: LGTM!Clean delegation to the generic termination helper with appropriate callbacks.
32-54: LGTM!Robust termination logic with proper exception handling for race conditions where the process may exit between checks. The
noqa: UP041annotation correctly documents the need forasyncio.TimeoutError.
57-81: LGTM!Cleanup correctly terminates processes before cancelling capture tasks, with exception suppression to ensure complete cleanup.
84-100: LGTM!Clean separation of concerns: this helper handles process termination and pipe result collection, leaving stream task cleanup to the caller.
103-111: LGTM!Returns a mutable dict as required by
subprocessAPIs. The|=merge operator is clean and idiomatic.
143-146: LGTM!Defensive check correctly ensures that callers provide explicit observations when observe hooks are registered, preventing accidental loss of observability wiring.
149-215: LGTM!Pipeline spawning correctly wires stdin/stdout between stages, records per-stage timing, emits start events, and handles cleanup on spawn failures.
218-229: LGTM!The use of
asyncio.shieldcorrectly protects the shared wait task from cancellation during the grace period timeout.
232-263: LGTM!Fail-fast termination correctly skips the failed stage and already-completed stages, terminating only the still-running processes.
cuprum/_pipeline_internals.py (10)
1-35: LGTM!Imports are well-organised with TYPE_CHECKING guards for type-only dependencies.
47-51: LGTM!Clean immutable data structure for hook collection with appropriate tuple types.
54-62: LGTM!Correctly collects hooks after enforcing the allowlist check. The function name reflects its role in the pre-execution phase.
65-97: LGTM!The
emitmethod correctly constructsExecEventwith all required fields. Note: duplication with_CommandObservationinsh.pywas flagged earlier.
100-105: LGTM!Simple data carrier for event emission details.
115-143: LGTM!Observations are correctly built with merged tags including context tags and pipeline metadata.
146-152: LGTM!Clean helper that emits plan events and executes before hooks in the correct order.
155-190: LGTM!Stage results are correctly constructed with duration calculation and exit event emission. The
max(0.0, ...)guard is appropriately defensive.
193-201: LGTM!Finalisation correctly runs after hooks and awaits pending async observe tasks.
276-284: LGTM!Clean iteration with
strict=Trueensuring length consistency between parts, hooks, and results.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Changed multiple internal pipeline data structures from list to tuple to reflect immutability (e.g., exit_codes, started_at, ended_at). - Updated relevant utility functions to handle these tuples correctly. - Replaced list literals with tuple literals in tests for consistency. - Improved error handling and code clarity by adding utility methods like _cancel_stream_tasks and _gather_optional_text_tasks. - Small cleanup and reorganization in submodules related to pipeline execution and observability. Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Summary
Introduce structured pipeline events and telemetry hooks to Cuprum. This enables emitting per-command and per-pipeline lifecycle events, line-based stdout/stderr observations, timing data, and merged metadata through a new observe API. Observers may be sync or async and are awaited to avoid leaking background tasks. Public API includes ExecEvent, ExecHook, and observe().
Changes
Tests
Migration and Usage
Example
Notes
🌿 Generated by Terry
ℹ️ Tag @terragon-labs to ask questions and address PR feedback
📎 Task: https://www.terragonlabs.com/task/85dd05a3-edc9-45f4-b76f-b12e7372e98c
Summary by Sourcery
Add structured execution event hooks and telemetry to command and pipeline execution, including per-phase events, line-based output observation, and tag-rich metadata, exposed via a new observe() API.
New Features:
Enhancements:
Documentation:
Tests: