Skip to content

feat(context): unify Context.create() signature per apcore #66#28

Merged
tercel merged 5 commits into
mainfrom
context-create-unify-issue-66
May 22, 2026
Merged

feat(context): unify Context.create() signature per apcore #66#28
tercel merged 5 commits into
mainfrom
context-create-unify-issue-66

Conversation

@tercel
Copy link
Copy Markdown
Contributor

@tercel tercel commented May 22, 2026

Implements the Python SDK side of apcore Issue #66.

Summary

  • New Context.create() signature: identity, trace_parent, cancel_token, data, services, global_deadline (kwargs-only, in spec order). executor= and caller_id= parameters removed.
  • New Context._bind_executor() private helper. New ContextBindingError (code CONTEXT_BINDING_ERROR).
  • Executor auto-binds at all entry points (call, call_async, stream, call_async_with_trace, _validate_async) — covers local construction, deserialized contexts, and hot-reload survivors uniformly per apcore §"Contract: Executor binding to Context".
  • 15-case conformance harness against apcore/conformance/fixtures/context_create.json.
  • Eliminated post-hoc ctx.cancel_token = token pattern across examples and tests.

Sub-finding: global_deadline distributed semantics fix

While removing the Context.create(executor=...) path, found that BuiltinContextStep only computed global_deadline in the "context is None" branch. Caller-supplied contexts (including deserialized ones from remote nodes) lost their deadline. Fixed by detecting root calls via empty call_chain and computing deadline in both branches — incidentally satisfying the new apcore §"Contract: global_deadline distributed semantics" MUST.

Test plan

  • pytest -q2999 passed / 0 failed / 2 skipped / 44 warnings on commit HEAD
  • All 15 conformance fixture cases pass
  • Context.create() no longer accepts executor= (signature-level removal)
  • Context.create() no longer accepts caller_id= (signature-level removal)
  • Auto-bind works for local create, deserialize, and same-executor idempotent rebind
  • Cross-executor rebind raises ContextBindingError
  • Context.child() propagates executor + cancel_token

Cross-references

  • apcore spec commits: 1753894, f819e34 (fixture refinement)
  • Sibling SDK PRs: apcore-typescript / apcore-rust (context-create-unify-issue-66 branches)

tercel added 5 commits May 21, 2026 14:00
Per apcore/CLAUDE.md, implementation repos contain only code and a
README — feature specs, test-case matrices, and design notes live in
the apcore protocol-spec repo. Removes:

  - docs/features/async-task-evolution.md
  - docs/features/middleware-architecture-hardening.md
  - docs/async-task-evolution/test-cases.md

These files were also stale (referenced the deprecated TaskStore.put
method and the removed TaskStatus.RETRYING enum value); the canonical
sources are the implementation in src/apcore/async_task.py and the
upstream spec at apcore/docs/features/async-tasks.md.

Signed-off-by: tercel <tercel.yi@gmail.com>
…nager surface

Implements four sync-audit findings against v0.22.0:

  * A-D-AT-04 / D-17 — TaskStore Protocol methods (save, get, delete,
    list, list_expired) are now async on the Protocol and on the default
    InMemoryTaskStore. AsyncTaskManager awaits all store calls; a
    transitional shim still drives sync stores so external custom stores
    get one deprecation window to migrate. cleanup() is async because
    list/delete now are.

  * A-D-AT-03 / D-11 — ReaperHandle.stop and AsyncTaskManager.stop_reaper
    are async and drain the reaper task before returning. Callers no
    longer need a manual `await asyncio.sleep(0)` after `handle.stop()`.

  * A-D-AT-06 — get_status / list_tasks return shallow copies via
    dataclasses.replace so callers cannot mutate the live store record.
    New get_status_async / list_tasks_async surfaces for I/O-backed
    stores that genuinely suspend.

  * A-D-AT-09 / D-14 — Legacy RetryPolicy now defaults max_retries to 0
    (retries are strictly opt-in across all SDKs) and emits a
    DeprecationWarning on instantiation steering callers to RetryConfig.

Adds tests/test_async_task_sync_audit_v022.py pinning each invariant
plus an async-only custom-store smoke test. Updates existing tests to
the new async surface (await store ops, await stop_reaper, await
cleanup) and registers an existing sync test as `@pytest.mark.asyncio`
where required.

Signed-off-by: tercel <tercel.yi@gmail.com>
…wrap

Two normative-decision fixes for the executor pipeline:

  * A-D-EXEC-002 / D-21 — BuiltinCallChainGuard now observes
    cancel_token.is_cancelled before any guard work and short-circuits
    with ExecutionCancelledError. Combined with the existing Step 8
    check this satisfies the two-point cancel-token invariant; the
    earlier single-check implementation leaked compute through
    ACL/middleware/validation even when the caller had already
    cancelled.

  * A-D-EXEC-005 / D-22 — Executor._recover_from_call_error now unwraps
    MiddlewareChainError and propagates its `.original` typed cause
    unchanged. Previously the wrapper was collapsed to a generic
    ModuleExecuteError, breaking callers that dispatch on typed errors
    (notably MCP/A2A bridges keying on APPROVAL_DENIED vs
    MODULE_EXECUTE_ERROR). Mirrors the TypeScript and Rust SDKs.

Adds tests/test_executor_sync_audit_v022.py with regression coverage
for both decisions. Drops the now-unused ModuleExecuteError import
from executor.py.

Signed-off-by: tercel <tercel.yi@gmail.com>
…ll paths

Four sync-audit findings clamp the Issue #65 invariants so they hold
uniformly across every registration path, not just the public
register() API:

  * A-D-REG-002 — register_internal's ephemeral-namespace check now
    uses the shared _is_ephemeral helper instead of bare .startswith,
    so the bare ID "ephemeral" is rejected too (it previously slipped
    through the .startswith("ephemeral.") guard).

  * A-D-REG-003 — _register_in_order (discover path) now follows the
    same three-phase deferred-publish protocol as register(): reserve
    in-flight slot → run on_load outside the lock → publish on
    success. Previously the discover path published into _modules
    BEFORE invoking on_load, leaving a window in which registry.get()
    callers could observe a module whose on_load-installed state was
    incomplete.

  * A-D-REG-004 — register_internal now uses the same three-phase
    protocol. The Issue #65 invariant ("modules invisible until
    on_load completes") now holds for sys-modules too.

  * A-D-REG-005 — _invoke_on_load now emits
    apcore.registry.module_load_failed when on_load fails, mirroring
    the public register() path so subscribers have a single hook for
    partial-init detection regardless of which registration route was
    used.

Adds tests/test_registry_sync_audit_v022.py with regression coverage
for all four findings, including a probe module whose on_load asserts
the module is NOT yet visible in the registry while the callback runs.

Signed-off-by: tercel <tercel.yi@gmail.com>
Implement the v0.22.0 unified Context.create() factory signature defined in
the apcore PROTOCOL_SPEC §"Contract: Context.create" and §"Contract: Executor
binding to Context" (Issue #66).

The factory now accepts exactly six caller-supplied fields in this order:
identity, trace_parent, cancel_token, data, services, global_deadline. The
previous `executor=` parameter is removed; the Executor binds itself to the
Context at pipeline entry via the new SDK-internal `Context._bind_executor`
helper. `cancel_token` and `global_deadline` are now first-class parameters,
eliminating the post-hoc `ctx.cancel_token = token` anti-pattern.

Same-instance rebinds are idempotent noops; cross-Executor rebinds raise the
new `ContextBindingError` (code `CONTEXT_BINDING_ERROR`). Auto-binding is
applied at every Executor entry point: call, call_async, stream,
call_async_with_trace, and the _validate_async dry-run path.

`BuiltinContextStep` now computes `global_deadline` for any root call
(empty call_chain) based on local executor.global_timeout, matching the
PROTOCOL_SPEC requirement that deserialized Contexts arriving at a remote
node MUST recompute global_deadline from local config.

Adds a new conformance harness (test_context_create_unified_signature)
driven by apcore/conformance/fixtures/context_create.json, covering all 15
canonical cases including create defaults, cancel_token / global_deadline
plumbing, executor binding rules, child propagation, deserialization
behavior, distributed cancel/deadline semantics, and tracestate inside
TraceParent.

Updates examples/cancel_token.py to the new shape and migrates ~25 test
call sites. Breaking for callers that previously passed `executor=` to
`Context.create()`; acceptable pre-release for v0.22.0.

Refs apcore#66

Signed-off-by: tercel <tercel.yi@gmail.com>
@tercel tercel merged commit 0ed8859 into main May 22, 2026
1 of 2 checks passed
@tercel tercel deleted the context-create-unify-issue-66 branch May 22, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant