Skip to content

Releases: aiperceivable/apcore-python

Release 0.20.0

05 May 07:10

Choose a tag to compare

Added

  • Pluggable OverridesStore interface (sync alignment, CRITICAL #1) — New apcore.sys_modules.overrides module exposes the OverridesStore Protocol with default InMemoryOverridesStore and FileOverridesStore (atomic YAML write via tempfile + os.replace) implementations, mirroring TypeScript's apcore-typescript/src/sys-modules/overrides.ts (OverridesStore / InMemoryOverridesStore / FileOverridesStore). register_sys_modules(..., overrides_store=...) accepts any OverridesStore; loaded overrides are applied to Config (and the live ToggleState for toggle.* keys) at startup, and UpdateConfigModule / ToggleFeatureModule persist back through the store on every successful mutation. The legacy sys_modules.control.overrides_path config key is retained as a backwards-compat shim that auto-constructs a FileOverridesStore. The previously-private _load_overrides helper now delegates to FileOverridesStore for symmetry. New top-level exports: OverridesStore, InMemoryOverridesStore, FileOverridesStore.

  • Public SubscriberFactory API (Issue #36)apcore.events.register_subscriber_factory(type_name, factory) and apcore.events.create_subscriber_from_config(config) (also re-exported from apcore) bring Python to parity with TypeScript's createSubscriberFromConfig / registerSubscriberFactory and Rust's create_subscriber / register_factory. Built-in factory types (webhook, a2a, file, stdout, filter) are auto-registered on import. The previously-private _create_subscriber helper remains for back-compat.

  • Pipeline StepMiddleware (Issue #33 §2.2) — Formal middleware mechanism for pipeline steps. New StepMiddleware Protocol exposes optional before_step(step_name, state), after_step(step_name, state, result), and on_step_error(step_name, state, error) hooks; both sync and async implementations are supported (return values detected via inspect.isawaitable(), mirroring the Issue #42 async on_error fix). ExecutionStrategy gains a step_middlewares: list[StepMiddleware] field plus an add_step_middleware() registration method. before_step and on_step_error run in registration order; after_step runs in reverse (onion semantics). A non-None return from on_step_error is treated as a recovery StepResult and execution continues normally; returning None lets the original exception propagate. Exported from apcore as StepMiddleware.

  • apcore.observability.batch_span_processor module (Issue #43 §2) — Dedicated module hosting the canonical BatchSpanProcessor and SimpleSpanProcessor implementations, mirroring the layout of the TypeScript (src/observability/batch-span-processor.ts) and Rust (src/observability/processor.rs) SDKs. Adds a synchronous force_flush(timeout_ms=30000) -> bool method that drains the queue while the processor remains alive (returns True once empty, False on deadline). shutdown() is now idempotent. Queue-full enqueues now log a (rate-limited) WARNING so dropped spans surface in operator logs rather than only via the spans_dropped counter. The classes remain re-exported from apcore.observability.tracing and apcore.observability for backward compatibility.

  • Generic pluggable StorageBackend (PROTOCOL_SPEC Issue #43 §1) — New apcore.observability.storage module defines a namespaced save / get / list / delete Protocol and the default InMemoryStorageBackend implementation, mirroring the shape of TaskStore. ErrorHistory, MetricsCollector, and UsageCollector now accept an optional storage: StorageBackend | None = None constructor argument; when supplied, error entries are mirrored to the backend's "errors" namespace keyed by fingerprint, enabling cross-process persistence. External backends (Redis, SQL, S3) remain user-supplied. Exports: StorageBackend, InMemoryStorageBackend from apcore.observability and the top-level apcore package.

  • TaskStore.list_expired(before_timestamp) (cross-language alignment D-10) — New method on the TaskStore Protocol returning terminal-state (COMPLETED / FAILED / CANCELLED) tasks whose completed_at precedes before_timestamp. Implemented on InMemoryTaskStore. Drives TTL-based reaper logic; non-terminal tasks are never returned. The method is REQUIRED on the Protocol — custom stores written before this release must add an implementation.

  • Registry.discover_multi_class(file_path, extensions_root="extensions") (cross-language alignment D-15) — New instance method on Registry wrapping the existing free function apcore.registry.multi_class.discover_multi_class. The registry's configured pre_approval_hook is forwarded to the underlying scanner so signature-verification and audit policies apply uniformly. The free function remains importable for existing callers; new code SHOULD prefer the method.

  • Granular reload via path_filter input in ReloadModule (#45.4) — Registry.discover(path_filter=...) accepts a glob string or list of patterns and only walks matching files; previously-registered modules outside the filter remain untouched. Patterns are matched (via pathlib.PurePath.match) against both the absolute file path and its path relative to each configured extension root.

  • Error fingerprinting in ErrorHistory — dedup by (error_code, top-frame hash, sanitized message template) (#43 §4). New compute_error_fingerprint(error, module_id) folds the deepest stack-frame file:lineno:func (basename only, for cross-machine stability) into the SHA-256 digest in addition to the existing code/module/normalized-message inputs. Long hex runs (≥ 8 chars) are now collapsed to <HEX> alongside the existing UUID/timestamp/integer placeholders. Legacy 3-arg compute_fingerprint retained.

  • Configurable redaction via obs.redaction.regex_patterns and obs.redaction.sensitive_keys Config keys (#43 §5). New obs namespace ships with sensible defaults (password, secret, token, api_key, authorization, cookie, _secret_*, …); operators can override via apcore.yaml. RedactionConfig.from_config(config) / RedactionConfig.default() build the runtime config; _secret_ prefix matching becomes a default entry rather than a hard-coded rule. Field-name match is case-insensitive substring with -/_/space normalization (so "X-API-Key" matches "api_key"); value-regex match is case-insensitive. apcore.utils.redaction.redact_sensitive accepts new keyword overrides (sensitive_keys, regex_patterns, replacement).

Changed

  • Event names normalized to apcore.<subsystem>.<event> form (#36) — Four legacy event types (module_registered, module_unregistered, error_threshold_exceeded, latency_threshold_exceeded) now also emit canonical aliases apcore.registry.module_registered, apcore.registry.module_unregistered, apcore.health.error_threshold_exceeded, apcore.health.latency_threshold_exceeded. Both forms are emitted during the deprecation window so existing subscribers keep working; the legacy emission carries deprecated: true in data. Glob subscribers using apcore.registry.* and apcore.health.* now match correctly. Deprecation: legacy bare names will be removed in v0.22.0.
  • Contextual auditing for system control modules (Issue #45.2) — Audit events emitted by system.control.update_config (apcore.config.updated), system.control.toggle_feature (apcore.module.toggled), and system.control.reload_module (apcore.module.reloaded) now include the requester's caller_id from context.caller_id (defaults to the @external sentinel when unset) and a redacted identity dict (id, type, roles) when context.identity is present.
  • Pipeline configuration is fail-fast (Issue #33 §1.2)build_strategy_from_config now raises ConfigurationError (new typed error, code PIPELINE_CONFIGURATION_ERROR) instead of logging a warning when YAML refers to a step that does not exist (in remove, configure, or insert.before/insert.after), assigns an unknown field via configure, or omits both after and before anchors on an inserted step. Misconfigurations now surface at start-up rather than producing inscrutable runtime failures.
  • Pipeline strategy dependency validation is fail-fast (Issue #33 §2.1)ExecutionStrategy.__init__ and insert_after/insert_before now raise PipelineDependencyError (new typed error, code PIPELINE_DEPENDENCY_ERROR) when a step's requires keys are not provided by any preceding step's provides. The error names the offending step and the missing keys. A new validate_dependencies: bool = True keyword on ExecutionStrategy.__init__ lets internal callers (e.g. Executor.stream's post-stream sub-strategy) opt out when assembling derived strategies from an already-validated parent. Both new errors are exported from apcore.
  • Cross-language alignment (sync A-001) — Renamed CircuitOpenError (code CIRCUIT_OPEN) to canonical CircuitBreakerOpenError (code CIRCUIT_BREAKER_OPEN) to match TypeScript and Rust SDKs and the protocol spec. The legacy CircuitOpenError class is retained as a deprecated subclass alias of CircuitBreakerOpenError so existing except CircuitOpenError: blocks raising the legacy class continue to work; the legacy class will be removed in a future major release. The wire error code emitted by CircuitBreakerMiddleware is now CIRCUIT_BREAKER_OPEN for both classes. New ErrorCodes.CIRCUIT_BREAKER_OPEN constant added; ErrorCodes.CIRCUIT_OPEN retained as a deprecated alias. CircuitBreakerOpenError is exported from the top-level apcore package.
  • TaskStore.putsave (cross-language alignment D-10) — Renamed the canonical write method on the TaskStore Protocol. InMemoryTaskStore.put is retained as a deprecated shim that delegates to save and emits a DeprecationWarning; it will be removed in a future minor release. Internal `AsyncTas...
Read more

Release 0.19.0

19 Apr 09:10

Choose a tag to compare

Added

  • DependencyNotFoundError (error code DEPENDENCY_NOT_FOUND) — raised by resolve_dependencies when a module's required dependency is not registered. Aligns Python with PROTOCOL_SPEC §5.15.2, which has always mandated this error code. Details include module_id and dependency_id. Exported from apcore.
  • DependencyVersionMismatchError (error code DEPENDENCY_VERSION_MISMATCH) — raised by resolve_dependencies when a declared version constraint is not satisfied by the registered version of the target module. Details include module_id, dependency_id, required, actual. Exported from apcore.
  • TaskLimitExceededError (error code TASK_LIMIT_EXCEEDED) — raised by AsyncTaskManager.submit when the manager is at capacity. Replaces the previous untyped RuntimeError and makes the failure dispatchable via error.code across language SDKs. Retryable=True.
  • VersionConstraintError (error code VERSION_CONSTRAINT_INVALID) — raised by matches_version_hint / VersionedStore callers on malformed constraint strings (empty, operator-without-operand, non-digit-leading operand such as "v1.0" or "latest"). Previously, malformed constraints silently degraded to (0,0,0) comparisons that always passed.
  • resolve_dependencies(..., module_versions=...) — new optional keyword argument mapping module_id → version_string. When provided, declared dependency version constraints are enforced per PROTOCOL_SPEC §5.3. When absent, the DependencyInfo.version field is silently ignored (back-compat for callers that do not wire versions through yet). ModuleRegistry._resolve_load_order now populates this map from YAML version / class version attr / DEFAULT_MODULE_VERSION ("1.0.0") fallback, and includes already-registered modules (from _versioned_modules) so inter-batch constraints resolve against the live registry's multi-version state — not just the latest-only primary map.
  • Caret (^) and tilde (~) constraint support in matches_version_hint / select_best_version (npm/Cargo semantics): ^1.2.3 → >=1.2.3,<2.0.0, ^0.2.3 → >=0.2.3,<0.3.0, ^0.0.3 → >=0.0.3,<0.0.4, ~1.2.3 → >=1.2.3,<1.3.0, ~1.2 → >=1.2.0,<1.3.0, ~1 → >=1.0.0,<2.0.0.
  • apcore.registry.registry.DEFAULT_MODULE_VERSION constant ("1.0.0") — canonical default applied by every registration path (register, _register_in_order, ModuleDescriptor.version fallback) for modules without an explicit version= argument or version class/instance attribute.
  • ExecutorProtocol — Protocol describing the minimal async-call surface required by AsyncTaskManager. Concrete Executor still satisfies it. Decouples the task manager from the concrete executor for testing.
  • Executor.close() plus sync (__enter__ / __exit__) and async (__aenter__ / __aexit__) context-manager support — releases the cached _sync_loop deterministically. Long-lived singleton executors can continue to ignore this; short-lived executors (per-request, per-test) should call close() or use with Executor(...) as executor:.
  • Registry.get_callback_errors(event=None) — public accessor returning the per-event callback-exception count recorded by _trigger_event. Ops can watch these counters to spot misbehaving subscribers. Event-callback exceptions remain logged + suppressed so the registry's register/unregister contract stays crash-free.

Fixed

  • resolve_dependencies cycle path accuracy_extract_cycle previously returned a phantom path (all remaining nodes plus the first one re-appended) when the arbitrarily-picked start node had no outgoing edge inside remaining. Rewritten to DFS from each remaining node (sorted) and return a true back-edge cycle [n0, ..., nk, n0]. When no back-edge exists (e.g., Kahn's sort stalled on a non-cycle blocker), the resolver now raises ModuleLoadError naming the blocked modules instead of emitting a CircularDependencyError with a phantom sorted(remaining) path.
  • Registry._register_in_order populates _versioned_modules and _versioned_meta — discover()-path modules were previously written only to the latest-only _modules map, leaving Registry.get(id, version_hint=…) unable to resolve them (version-hint queries route through the versioned store first). All registration paths now produce equivalent state.
  • Default version alignmentRegistry.register() with no version= now falls back to DEFAULT_MODULE_VERSION ("1.0.0"), matching _resolve_load_order and ModuleDescriptor.version. Previously register() used "0.0.0" while _resolve_load_order used "1.0.0" — the same module routed through the two paths got different effective versions.
  • Non-string versions warn-and-coerce — a module with version: 1 in YAML (integer) or a numeric class attribute is no longer silently dropped from constraint enforcement. The resolver logs a WARN naming the module and coerces via str(...).
  • Malformed version constraints raise VersionConstraintError — previously ">=not_a_version", "v1.0", and "~" alone passed _CONSTRAINT_RE and degraded to (0,0,0) comparisons that always returned True. The constraint regex now requires a digit-leading operand, and _check_single_constraint raises explicitly on malformed input.
  • Scanner confines follow_symlinks=True to the extension root — symlinks whose real path escapes the root (e.g., into /etc, $HOME, a sibling project) are now refused with a WARN log. The previous code would walk any target once per real-path visit. Combined with a WARN log on the first discover() when follow_symlinks=True is configured, this makes the trust boundary visible.
  • Executor._run_in_new_thread bounds thread.join() by _global_timeout — a dead-locked coroutine can no longer indefinitely hang the sync caller. On timeout the daemon thread is left running (process exit stays clean) and the caller receives a ModuleTimeoutError.
  • Executor.stream post-stream validation failures log at WARNING (not DEBUG) — unvalidated output that already reached the consumer is worth investigating even if it can't be un-sent. Previously such failures were invisible in default production observability.
  • AsyncTaskManager.cancel narrows its except — catches asyncio.CancelledError specifically (the expected cancellation path). Unexpected exceptions from the cancelled task are now logged at WARNING with a stack trace instead of being silently swallowed alongside CancelledError.
  • errors.py __all__ — adds DependencyNotFoundError, DependencyVersionMismatchError, TaskLimitExceededError, VersionConstraintError. from apcore.errors import * now picks them up.
  • Inline __import__('time') / __import__('os') in _ModuleChangeHandler replaced with top-level imports. No behavior change.

Changed (BREAKING)

  • Missing required dependencies now raise DependencyNotFoundError (code DEPENDENCY_NOT_FOUND) instead of ModuleLoadError (code MODULE_LOAD_ERROR). Brings Python into compliance with PROTOCOL_SPEC §5.15.2 which has always mandated DEPENDENCY_NOT_FOUND. Upgrade path: catch DependencyNotFoundError specifically, or catch the ModuleError base class for any dependency-related failure. The error-code-based dispatch (via ErrorCodes.DEPENDENCY_NOT_FOUND) also works and is recommended for cross-language consumers.
  • AsyncTaskManager.submit now raises TaskLimitExceededError, not RuntimeError. Callers catching RuntimeError("Task limit reached …") must migrate to except TaskLimitExceededError: (or catch ModuleError for any task-manager failure).
  • Registry.register() default version is now "1.0.0" instead of "0.0.0" for modules registered without an explicit version= argument and without a class/instance version attribute. Callers that relied on "0.0.0" as an "unset" marker must pass version="0.0.0" explicitly. ModuleDescriptor.version has always defaulted to "1.0.0" — this aligns the internal state with the externally-visible view.
  • Malformed version constraint strings now raise VersionConstraintError instead of silently evaluating to False (which, via the degraded-parse-semver path, effectively became True). Callers that relied on silent no-op behavior must wrap in try/except or sanitize upstream.
  • Dependency upper boundspydantic, pyyaml, and jsonschema are now pinned to <3, <7, <5 respectively. Prevents silent breakage from downstream major releases; raise the cap deliberately after a compatibility check.

Added

  • DECLARATIVE_CONFIG_SPEC.md v1.0 — Canonical spec for bindings, pipeline config, and entry-point YAML. Lives in apcore/docs/spec/. Defines cross-SDK YAML syntax parity, error model, configurable policy limits, and auto_schema semantics. All three SDKs (Python, TypeScript, Rust) now conform to this unified specification.
  • auto_schema: true | permissive | strict — Explicit auto-schema mode selection. strict mode enforces OpenAI/Anthropic-compatible schemas (additionalProperties: false, all properties required, restricted type set). Incompatible features produce BindingStrictSchemaIncompatibleError at parse time.
  • auto_schema as implicit default — When no schema mode is specified in a binding entry, auto-schema inference is attempted automatically. This formalizes Python's existing behavior as cross-SDK spec.
  • Schema mode conflict detection — Specifying multiple schema modes (e.g., auto_schema + input_schema) now produces BindingSchemaModeConflictError at parse time.
  • spec_version field in binding YAML files. Defaults to "1.0" with deprecation warning when absent (mandatory in spec 1.1).
  • New canonical error classes: BindingSchemaInferenceFailedError, BindingSchemaModeConflictError, BindingStrictSchemaIncompatibleError, BindingPolicyViolationError. Each includes file path, line, module ID, and s...
Read more

Release 0.18.0

15 Apr 02:18

Choose a tag to compare

Added

  • APCore unified client class (apcore.client.APCore) — High-level facade over Registry + Executor providing a single entry point for all module operations. Constructor accepts optional registry, executor, config, and metrics_collector (auto-created when sys_modules.enabled). Public API surface:
  • Module management: module() decorator, register(), list_modules(tags=, prefix=), discover(), describe()
  • Execution: call(), call_async(), stream(), validate() — all accept version_hint for semver negotiation (A14)
  • Middleware: use(), use_before(), use_after(), remove()use/use_before/use_after return self for chaining
  • Events: events property, on(event_type, handler), off(subscriber) — requires sys_modules.events.enabled in config
  • Module toggle: disable(module_id, reason=), enable(module_id, reason=) — wrappers around system.control.toggle_feature
  • Cross-language parity: matches apcore-typescript APCore class and apcore-rust APCore struct public API surface
  • Package-level global convenience functions (apcore.call, apcore.call_async, apcore.stream, apcore.validate, apcore.register, apcore.describe, apcore.use, apcore.use_before, apcore.use_after, apcore.remove, apcore.discover, apcore.list_modules, apcore.on, apcore.off, apcore.disable, apcore.enable, apcore.module) — delegate to a module-level _default_client = APCore() instance for zero-setup usage (import apcore; apcore.call("math.add", {"a": 1, "b": 2})). Python-specific ergonomic; apcore-typescript and apcore-rust require explicit client construction.
  • Pipeline preset builders re-exported at package rootbuild_standard_strategy, build_internal_strategy, build_testing_strategy, build_performance_strategy, build_minimal_strategy are now importable directly from apcore. These functions existed in apcore.builtin_steps but were not previously in apcore.__all__. Parity with apcore-typescript (buildXxxStrategy) and apcore-rust (build_xxx_strategy at the crate root).
  • TestRegisterInternalValidation test class in tests/registry/test_registry.py (6 parity tests covering empty rejection, pattern rejection, over-length rejection, reserved-word bypass, duplicate rejection, accept-at-max-length) plus test_pipeline_preset_builders_* in tests/test_public_api.py.
  • Registry.export_schema() — New method returning a module's schema definition as a dict, with optional strict=True for OpenAI/Anthropic strict schema compliance (additionalProperties: false). Aligned with apcore-rust Registry::export_schema().

Changed

  • Executor.describe_pipeline() now returns StrategyInfo instead of str — Provides structured access to step_count, step_names, name, and description. str(result) produces the original formatted string via StrategyInfo.__str__. Aligned with apcore-typescript describePipeline() -> StrategyInfo.

  • Executor.call() and Executor.call_with_trace() are now thin sync wrappers over call_async() and call_async_with_trace() via a shared _run_async_in_sync(coro, module_id) dispatcher. The cached-event-loop / thread-bridge logic that was previously inlined in three places lives in one helper. Sync semantics preserved: nested calls inside a running event loop still route through a background thread.

  • Executor.call_async_with_trace() now uses the unified A11 error recovery path (_translate_abort + _recover_from_call_error + middleware on_error chain). Previously it called engine.run raw and let PipelineAbortError leak; behavior now matches call_async. When a middleware on_error recovers, the recovery dict is returned alongside a sentinel PipelineTrace (per-step trace detail is unavailable in the recovery branch — use call_async if you don't need the trace, or attach a tracing middleware).

  • BuiltinApprovalGate now self-contains the full approval flow. Audit-log emission, span-event emission, and full status→error mapping (including timeout and unknown-status warning) used to live on private methods of Executor, with BuiltinApprovalGate reaching into them via hasattr(executor, '_check_approval_async'). The reach-into-private cheat is gone; BuiltinApprovalGate does everything itself. The executor= parameter on BuiltinApprovalGate.__init__ is removed (was unused after consolidation). Approval audit logs are now emitted from logger apcore.builtin_steps (was apcore.executor) — update any log filters accordingly.

  • BuiltinACLCheck and BuiltinApprovalGate now expose public set_acl() / set_handler() setters. Executor.set_acl and set_approval_handler use the public setters instead of poking step ._acl / ._handler. Custom user-supplied ACL or approval steps without these setters are silently skipped — re-register the strategy if you need to swap providers on a custom step.

  • Registry._discover_default() decomposed from a 153-line god method into a 23-line orchestrator + 9 named stage helpers (_scan_params, _scan_roots, _apply_id_map_overrides, _load_all_metadata, _resolve_all_entry_points, _validate_all, _resolve_load_order, _filter_id_conflicts, _register_in_order, _invoke_on_load). Pure refactor — no behavior change. Mirrors the structure of apcore-typescript's _discoverDefault.

  • ACL.check() and ACL.async_check() consolidated via shared _snapshot() and _finalize_check() helpers. Audit-entry construction and debug-logging now live in exactly one place (was duplicated four times). Fixed _matches_rule_async to call _match_patterns() instead of inlining a variant that bypassed compound operators ($or/$not).

  • ACL singular condition handler aliases removed (identity_type, role, call_depth). Spec §6.1 only defines the plural forms (identity_types, roles, max_call_depth); the singular aliases were a python-only divergence.

  • builtin_steps.py strategy builders no longer use object.__setattr__ for the name field. ExecutionStrategy was never a frozen dataclass — s.name = X always worked. Cargo-cult code removed.

  • ErrorCodes class __setattr__/__delattr__ traps dropped. The traps only fired on instance attribute mutation (ErrorCodes().X = ...), never on class attribute mutation (ErrorCodes.X = ...) which is how ErrorCodes is actually used. Cargo-cult immutability that gave a false sense of protection. Aligned with apcore-typescript (Object.freeze) and apcore-rust (enum).

  • Pydantic v1/v2/dataclass/constructor fallback cascade collapsed in config.py. Previously maintained a 4-branch compatibility chain for Pydantic v1 → v2 migration. The project requires Pydantic v2 since 0.16.0; dead branches removed.

  • Registry._handle_file_change() refactored — replaced fragile dir(mod) module-attribute discovery with explicit registry lookup. More predictable behavior on hot-reload events.

  • Registry.register() / register_internal() now populate _module_meta at registration time, not lazily at first get_definition() call. Consistent with _discover_default path.

  • 31 pre-existing pyright type errors resolved across executor.py, config.py, registry.py, builtin_steps.py, and acl.py. No runtime behavior change; strict type-checking now passes cleanly.

  • MAX_MODULE_ID_LENGTH raised from 128 to 192 (apcore.registry.registry). Tracks PROTOCOL_SPEC §2.7 EBNF constraint #1 update — accommodates Java/.NET deep-namespace FQN-derived IDs while remaining filesystem-safe (192 + len('.binding.yaml') = 205 < 255-byte filename limit on ext4/xfs/NTFS/APFS/btrfs). Module IDs valid before this change remain valid; only the upper bound moved. Forward-compatible relaxation: older 0.17.x/0.18.x readers will reject IDs in the 129–192 range emitted by this version.

  • Registry.register() and Registry.register_internal() now share a _validate_module_id() helper that runs validation in canonical order (empty → EBNF pattern → length → reserved word per-segment). The reserved-word check is the only step register_internal() skips (so sys modules can use the system.* prefix); empty/pattern/length/duplicate now apply uniformly. Aligned cross-language with apcore-typescript and apcore-rust.

  • register_internal() now enforces empty / pattern / length / duplicate checks. Previously bypassed every validation step. Production callers (apcore.sys_modules.*) all use canonical-shape IDs so no in-tree caller is broken; external adapters that used register_internal as a generic escape hatch should review.

  • Duplicate registration error message canonicalized to "Module ID '<id>' is already registered" (was "Module already exists: <id>" for register_internal). Both register() and register_internal() now emit the same message via the shared error path. Aligned with apcore-rust and apcore-typescript byte-for-byte.

Removed

  • FeatureNotImplementedError and DependencyNotFoundError — zero raise-sites across the codebase; grep -rn confirmed no production or test code instantiated either class. Error codes GENERAL_NOT_IMPLEMENTED and DEPENDENCY_NOT_FOUND remain in ErrorCodes for use via the generic ModuleError constructor. Aligned with apcore-typescript (commit 01ea84d).

Removed (BREAKING)

  • Context.to_dict() and Context.from_dict() — superseded by the spec-compliant Context.serialize() and Context.deserialize() (shipped in v0.16.0). The two pairs were silently inconsistent (to_dict always emitted redacted_inputs even when None while serialize omitted it; serialize included _context_version: 1, to_dict did not), so mixing them produced divergent dicts. Migration:
  • ctx.to_dict()ctx.serialize()
  • Context.from_dict(data, executor=x)Context.deserialize(data); ctx.executor = x (the `exec...
Read more

Release 0.17.1

06 Apr 04:59

Choose a tag to compare

Added

  • build_minimal_strategy() — 4-step pipeline (context → lookup → execute → return) for pre-validated internal hot paths. Registered as "minimal" in Executor preset builders.
  • requires / provides on BaseStep — Optional advisory fields declaring step dependencies. ExecutionStrategy validates dependency chains at construction and insertion, emitting warnings for unmet requires.

Fixed

  • "minimal" added to preset buildersExecutor(strategy="minimal") now works. Previously missing from _resolve_strategy_name() preset dict.
  • Executor docstrings updated — Constructor and _resolve_strategy_name docstrings now list all 5 presets (was missing "minimal").

Release 0.17.0

05 Apr 11:30

Choose a tag to compare

Added

  • Step Metadata: Four declarative fields on BaseStep: match_modules (glob patterns for selective execution), ignore_errors (fault-tolerant steps), pure (safe for validate() dry-run), timeout_ms (per-step timeout).
  • YAML Pipeline Configuration: register_step_type(), unregister_step_type(), registered_step_types(), build_strategy_from_config() — configure pipeline steps via apcore.yaml at startup.
  • PipelineContext fields: dry_run, version_hint, executed_middlewares for pipeline-aware execution.
  • StepTrace: skip_reason field for understanding why steps were skipped ("no_match", "dry_run", "error_ignored").

Changed

  • Step order: middleware_before now runs BEFORE input_validation (was after). Middleware input transforms are now validated by the schema check.
  • Executor delegation: call(), call_async(), validate(), and stream() fully delegate to PipelineEngine.run(). Removed ~300 lines of duplicated inline step code.
  • Renamed: safety_check step → call_chain_guard (accurately describes call-chain depth/cycle/repeat checking).
  • Renamed: BuiltinSafetyCheck class → BuiltinCallChainGuard.

Fixed

  • Middleware input transforms were never re-validated against the schema (now validated after middleware runs).
  • validate() was hardcoded to 7 inline checks; now uses dry_run=True pipeline mode — user-added pure=True steps automatically participate.

Release 0.16.0

05 Apr 03:30

Choose a tag to compare

Added

  • Config Bus: env_style (auto/nested/flat), max_depth, env_prefix auto-derivation, env_map (namespace + global), Config.env_map(), CONFIG_ENV_MAP_CONFLICT error.
  • Context: ContextKey[T] typed accessor with get()/set()/delete()/exists()/scoped(). Built-in key constants (TRACING_SPANS, METRICS_STARTS, etc.). Context.serialize()/deserialize() with _context_version: 1.
  • Annotations: extra: dict[str, Any] extension field on ModuleAnnotations. pagination_style changed from Literal to str. DEFAULT_ANNOTATIONS constant. from_dict() classmethod with unknown key capture.
  • ACL: SyncACLConditionHandler / AsyncACLConditionHandler protocols. ACL.register_condition(). $or/$not compound operators. async_check() method. Fail-closed for unknown conditions.
  • Pipeline: Step protocol, BaseStep ABC, StepResult, PipelineContext, PipelineTrace, ExecutionStrategy, PipelineEngine. 11 BuiltinStep classes. Preset strategies (standard/internal/testing/performance). Executor.strategy parameter. call_with_trace()/call_async_with_trace(). register_strategy()/list_strategies()/describe_pipeline().

Changed

  • Middleware data keys migrated from legacy names (_metrics_starts etc.) to _apcore.mw.* convention using typed ContextKey.

Release 0.15.1

31 Mar 10:11

Choose a tag to compare

Changed

  • Env prefix convention simplified — Removed the ^APCORE_[A-Z0-9] reservation rule from Config._validate_env_prefix(). Sub-packages now use single-underscore prefixes (APCORE_MCP, APCORE_OBSERVABILITY, APCORE_SYS) instead of the double-underscore form. Only the exact APCORE prefix is reserved for the core namespace.
  • Built-in namespace env prefixes: APCORE__OBSERVABILITYAPCORE_OBSERVABILITY, APCORE__SYSAPCORE_SYS.

Release 0.15.0

31 Mar 06:43

Choose a tag to compare

Added

Config Bus Architecture (§9.4–§9.14)

  • Config.register_namespace(name, schema=None, env_prefix=None, defaults=None) — Class-level namespace registration. Any package can claim a named config subtree with optional JSON Schema validation, env prefix, and default values. Global registry is shared across all Config instances. Late registration is allowed; call config.reload() afterward to apply defaults and env overrides.
  • config.get("namespace.key.path") — Dot-path access with namespace resolution. First segment resolves to a registered namespace; remaining segments traverse the subtree.
  • config.namespace(name) — Returns the full config subtree for a registered namespace as a dict.
  • config.bind(ns, type) / config.get_typed(path, type) — Typed namespace access; bind returns a view of the namespace deserialized into type, get_typed deserializes a single dot-path value.
  • config.mount(namespace, from_file=...|from_dict=...) — Attach external config sources to a namespace without a unified YAML file. Primary integration path for third-party packages with existing config systems.
  • Config.registered_namespaces() — Class-level introspection; returns names of all registered namespaces.
  • Unified YAML with namespace partitioning — Single YAML file with namespace-keyed top-level sections. Automatic mode detection: legacy mode (no apcore: key, fully backward compatible) vs. namespace mode (apcore: key present). _config is a reserved meta-namespace (strict, allow_unknown).
  • Per-namespace env override with longest-prefix-match dispatch — Each namespace declares its own env_prefix. APCORE__ double-underscore convention for apcore sub-packages (e.g., APCORE__OBSERVABILITY, APCORE__SYS) to avoid collision with the existing single-underscore APCORE_ prefix used for flat keys.
  • Hot-reload namespace supportconfig.reload() re-reads YAML, re-detects mode, re-applies namespace defaults and env overrides, re-validates, and re-reads mounted files.
  • New error codesCONFIG_NAMESPACE_DUPLICATE, CONFIG_NAMESPACE_RESERVED, CONFIG_ENV_PREFIX_CONFLICT, CONFIG_MOUNT_ERROR, CONFIG_BIND_ERROR

Error Formatter Registry (§8.8)

  • ErrorFormatter protocol — Interface for adapter-specific error formatters. Implementations transform ModuleError into the surface-specific wire format (e.g., MCP camelCase, JSON-RPC code mapping).
  • ErrorFormatterRegistry — Shared registry for surface-specific formatters:
  • ErrorFormatterRegistry.register(surface, formatter) — register a formatter for a named surface
  • ErrorFormatterRegistry.get(surface) — retrieve a registered formatter
  • ErrorFormatterRegistry.format(surface, error) — format an error, falling back to error.to_dict() if no formatter is registered for that surface
  • New error codeERROR_FORMATTER_DUPLICATE

Built-in Namespace Registrations (§9.15)

  • observability namespace (APCORE__OBSERVABILITY env prefix) — apcore pre-registers this namespace, promoting the existing apcore.observability.* flat config keys (tracing, metrics, logging, error_history, platform_notify) into a named subtree. Adapter packages (apcore-mcp, apcore-a2a, apcore-cli) should read from this namespace rather than independent logging defaults.
  • sys_modules namespace (APCORE__SYS env prefix) — apcore pre-registers this namespace, promoting the existing apcore.sys_modules.* flat keys into a named subtree. register_sys_modules() prefers config.namespace("sys_modules") in namespace mode with config.get("sys_modules.*") legacy fallback. Both registrations are 1:1 migrations of existing keys; there are no breaking changes.

Event Type Naming Convention and Collision Fix (§9.16)

  • Canonical event names — Two confirmed event type collisions in apcore-python are resolved:
  • "module_health_changed" (previously used for both enable/disable toggles and error-rate recovery) split into apcore.module.toggled (toggle on/off) and apcore.health.recovered (error rate recovery)
  • "config_changed" (previously used for both key updates and module reload) split into apcore.config.updated (runtime key update via system.control.update_config) and apcore.module.reloaded (hot-reload via system.control.reload_module)
  • Naming conventionapcore.* is reserved for core framework events. Adapter packages use their own prefix: apcore-mcp.*, apcore-a2a.*, apcore-cli.*.
  • Transition aliases — All four legacy short-form names (module_health_changed, config_changed) continue to be emitted alongside the canonical names during the transition period.

Release 0.14.0

25 Mar 02:09

Choose a tag to compare

Added

  • Middleware priorityMiddleware base class now accepts priority: int (0-1000, default 0). Higher priority executes first; equal priority preserves registration order. BeforeMiddleware and AfterMiddleware adapters also accept priority.
  • Priority range validationValueError raised for priority values outside 0-1000

Breaking Changes

  • Middleware default priority changed from 0 to 100 per PROTOCOL_SPEC §11.2. Middleware without explicit priority will now execute before priority-0 middleware.

Release 0.13.2

22 Mar 12:51

Choose a tag to compare

Changed

  • Rebrand: aipartnerup → aiperceivable