Normative public Python surfaces for embedding Weft. Operation meaning is owned by the vertical specs; this document owns which names are public, the stability policy, result/error contracts, and surface layering.
| Surface | Import form | Role |
|---|---|---|
weft.client |
package (__all__) |
Primary embedder interface for application logic. |
weft.ext |
module (__all__) |
Extension and downstream contracts. |
weft.commands |
package (__all__, lazy facade) |
CLI-equivalent adapter for process and CLI reuse. |
Each __all__ is its authoritative public-name inventory. Names not exported
there, including weft.core.*, helpers, constants, command leaves, and
execute_run, are private.
weft.ext.__all__ exports RunnerHandle, RunnerCapabilities,
RunnerRuntimeDescription, AgentResolverResult, AgentToolProfileResult,
AgentMCPServerDescriptor, RunnerEnvironmentProfileResult, AgentResolver,
AgentToolProfile, RunnerEnvironmentProfile, TaskRunnerBackend,
RunnerPlugin, SpecRunInputRequest, ResourceMetrics, RunnerOutcome,
SessionExecutionResult, NormalizedAgentMessage, NormalizedAgentWorkItem,
CommandSessionProtocol, and AgentSessionProtocol.
weft.client.__all__ retains its existing inventory and adds exactly
CommandError, CommandUsageError, CommandTimeoutError,
CommandExecutionError, SubmissionError, SubmissionValidationError,
SubmissionManagerError, and normalize_taskspec_payload.
The client additionally exports TaskSpec, SpecSection, IOSection,
StateSection, LimitsSection, RunnerSection, ReservedPolicy,
AgentSection, AgentTemplateSection, AgentToolSection,
ParameterizationSection, ParameterizationArgumentSection,
RunInputSection, RunInputArgumentSection, RunInputStdinSection,
WeftContext, and build_context. These are the existing validated models
and context owner, not parallel representations. Supported TaskSpec use is
construction, validation, field inspection, and serialization. Lifecycle and
transport helpers remain runtime internals. Template validation uses
TaskSpec.model_validate(payload, context={"template": True, "auto_expand": False}); normal construction retains resolved expansion.
Submission remains responsible for committing the task and its TID.
Ordinary WeftClient submission uses weft.spawn.requests. Same-context runtime identity supplies system-owned root, submitter and finite ancestry without a future child count. Caller metadata cannot grant ancestry or internal authority. Outside that runtime or context, submissions are roots. Client sessions do not change result semantics or task limits. [MA-1.8] states the finite independent-subtree progress conditions; arbitrary cross-task waits have no such guarantee.
build_context retains its existing arguments and side effects: by default
it resolves configuration and creates the metadata directories and broker;
create_dirs=False and create_database=False disable those respective
creation steps; resolution can still create the project root. A supplied
context is preserved by WeftClient. The context does not own a permanently
open broker; callers close queues they obtain and use the broker context
manager to release its resources. An explicitly entered WeftClient may own
one lazy BrokerSession lease for its exact resolved context and Config
identity. Submission operations borrow that lease but keep each connection
checkout and transaction bounded. An unentered or closed client retains
bounded per-call session ownership. The context's config
and broker_config are immutable SimpleBroker Config snapshots with uppercase
unprefixed keys, as specified in [SB-0.4]. Create a new context to apply changed
configuration; copying its values does not reconfigure live handles. Broker
types remain SimpleBroker's public contracts.
WeftContext.session() returns a new SimpleBroker BrokerSession bound to the
context's resolved target and Config. It is intended for a with block
entered and exited by the executing thread. WeftContext does not cache a live
session. WeftClient.__enter__() establishes same-process, same-thread
ownership but creates the retained session lazily on first same-context
submission. close() releases that lease on the owning thread. Failed close
retains cleanup ownership and blocks submission or re-entry until the same
owner retries successfully. Sequential re-entry is allowed; active nesting and
active-scope submission, re-entry, and close from a foreign thread are rejected.
Preparation and observation retain their existing ownership contracts. A
forked child first abandons the inherited handle through SimpleBroker's public
child-safe BrokerSession.close() path, then uses child-owned bounded state
unless explicitly re-entered in the child. Successful close is idempotent and
returns the client to bounded behavior. Already returned Task and
PreparedSubmission handles remain usable after close.
WeftClient.__exit__() preserves an active body exception over ordinary close
failure by attaching cleanup diagnostics; with no body exception the close
failure propagates. Non-ordinary cleanup exceptions retain SimpleBroker's
priority. Retention spans the session lease and its single lazy spawn-queue
facade. It never spans a queue
operation, backend checkout, SQL transaction, manager recovery, or readiness
wait. Existing queue() and broker() ownership and defaults are unchanged.
Implementation: weft/client/_client.py::WeftClient and
weft/commands/submission.py::_submit_with_session.
Related plan: Burst result observation.
Implementation mapping: weft/context.py::WeftContext.session owns the
factory. weft/client/_client.py::WeftClient owns the retained client
lifecycle. Exact target/config behavior is covered by
tests/context/test_context.py; client ownership is covered by
tests/core/test_client.py and tests/core/test_public_client_contracts.py;
real-backend task/session cleanup is covered by
tests/core/test_task_runtime_connections.py.
Related plan: 2026-09-14-simplebroker-8-2-configuration-plan (retired — source 396c3b85; see docs/plans/README.md).
Related plan: Explicit broker session lifetimes.
Extension value types preserve their existing fields and behavior.
CommandSessionProtocol exposes readonly pid, handle, last_metrics;
send(data), close_stdin(), poll_stdout(), poll_stderr(), is_alive(),
returncode(), terminate(*, deadline=None), close(), poll_limits(), and
stop_monitor(). AgentSessionProtocol exposes readonly pid, handle,
execute(work_item, *, cancel_requested=None) -> SessionExecutionResult, and
close(*, deadline=None). Backend session methods return these structural
protocols; concrete process session constructors remain private. These types
preserve existing lifecycle semantics and introduce no new cleanup guarantee.
Agent callbacks receive the AgentSection publicly available from
weft.client and normalized work-item types from weft.ext.
First-party runners may still use private implementation under coordinated
versioning; these exports do not promise a complete standalone backend SDK.
TaskRunnerBackend.start_session(*, on_activity) requires a zero-argument
callback and returns CommandSessionProtocol. An interactive backend publishes
session-observable stream or tracked-runtime-exit state before invoking the
callback. TaskRunner.start_session() forwards it unchanged.
CommandSessionProtocol gains no notification registration or wait method.
Implementation: weft/client/__init__.py re-exports the schema from
weft/core/taskspec/model.py and context from weft/context.py.
weft/ext.py owns the public extension values and protocols;
weft/core/tasks/runner.py, weft/core/tasks/consumer.py, and
weft/core/tasks/interactive.py consume the structural session contracts.
WeftClient.from_context(spec_context=None, *, fallback_root=None, autostart=None)
requests a resolved context from the core context owner. fallback_root follows
[SB-0.4] and is not an explicit override of a parameterized TaskSpec reference's
declared context. An explicit path argument, caller-supplied WeftContext, or resolved
Config CONTEXT is explicit. Direct WeftClient() construction and the factory agree
about this policy. Clients preserve supplied contexts and use their resolved Config
snapshots; the Django adapter does not set core's context-precedence bookkeeping.
Implementation mapping: weft/client/_client.py::WeftClient.__init__,
weft/client/_client.py::WeftClient.from_context, weft/context.py::build_context,
weft/client/__init__.py, weft/commands/__init__.py, weft/ext.py,
weft/core/taskspec/model.py, tests/core/test_client.py,
tests/core/test_public_client_contracts.py,
tests/core/test_public_extension_contracts.py, and
tests/fixtures/public_extension_contract.py.
Task observations use task-owned per-TID snapshots and bound observation
configuration under [MF-5]. Waiting observers register exact task-state interests
before their read/check/wait cycle. Pure point queries use a broker read without
an activity subscription.
System status, task list, and stats default to current nonterminal inventory;
include_terminal=True adds retained terminal projections. include_log_history=True
explicitly unions retained raw history. System status exposes
include_terminal=False, status=None, and include_log_history=False; task
list/stats retain their existing selection arguments and add
include_log_history=False. Bulk inventory may lag completion after failed
publication or abrupt child death; known-TID local evidence keeps its authority.
Ordinary observation never queries Monitor tables. Missing configuration follows
[MF-5]'s unknown/finite-timeout contract; unresolved controls fail before writing.
Accepted handles retain their immutable configuration after metadata retirement.
Explicit event streams provide retained/live raw events without lossless history.
Every canonical CLI verb has one actual implementation exported from
weft.commands: cmd_ plus its full CLI path, joined with underscores and
hyphens normalized to underscores. The initial bijection is 41 verbs: 4 root,
14 queue, 6 spec, 6 task, 5 manager, and 6 system. The root callback and
global --version are parser features, not verbs. The package resolves exports
lazily and caches them.
Commands accept parsed semantic values, perform semantic validation and
orchestration, return structured outcomes, and raise typed errors. They do not
read process stdin or write process output. weft.cli owns shell decoding,
human/JSON formatting, stream routing, and exit translation. Each Typer verb
invokes its corresponding facade export exactly once.
Canonical CLI argument/long-option spellings become parameter names by
replacing hyphens with underscores; callback-local names and short aliases do
not govern them. Repeatable options retain the singular spelling and accept a
Sequence; dual flags use the positive semantic spelling (apply, follow).
Positional arguments are positional-or-keyword and semantic options are
keyword-only. No command accepts **kwargs. Presentation-only --json,
--quiet, --verbose, --error, and --timestamps are excluded. Task-list
--stats is also presentation-only; queue-list --stats remains semantic.
Submission receipts and returned Task objects prove broker acceptance, not
manager readiness or completed execution. Confirmed acceptance survives a
post-write readiness failure without changing receipt fields or assigning a
second TID. Readiness-only failures are availability diagnostics rather than
submission errors. An authoritative manager rejection retains its typed error
and includes the accepted TID and rejection reason in the message. Direct
manager lifecycle commands continue to require readiness proof.
A returned submission handle retains access to the accepted canonical task configuration, assigned TID, and effective resolved context needed by the shared result capability [MF-5]. These immutable values are usable before child initialization and after the submitting client closes. They are not a lifecycle snapshot. Python and CLI callers observe and retrieve task results directly through that capability; result meaning and errors remain shared. Bare-TID callers use the task-specific discovery boundary in [MF-5].
Implementation mapping: weft/core/spawn_requests.py::_submit_spawn_request_with_payload
retains the committed transport; weft/commands/submission.py::_bind_accepted_taskspec
binds it after acceptance/recovery; weft/client/_task.py::_task_with_configuration
and weft/commands/run.py::_LiveRunSession retain it. Queue-based custody
coverage lives in tests/commands/test_submission_custody.py,
tests/core/test_client.py, tests/commands/test_run.py, and
tests/commands/test_run_public.py. Snapshot custody coverage lives in
tests/commands/test_point_snapshot_loans.py; its owner is
weft/client/_task.py::Task.snapshot, through the shared point-snapshot capability.
tests/commands/test_observer_command_failures.py covers typed observer failure
and cleanup priority at public boundaries.
weft/commands/result.py::_task_result_event projects the shared settled-result
shape for consuming follow and stream adapters.
Related plan: Direct result observation spec changes. Implementation repair plan: Direct result observation. Related plan: Result observation simplification.
The exact signature exceptions are: cmd_run has spec_args=(),
describe=False, and stdin_text=None;
cmd_queue_write(queue_name, message=None, *, endpoint=None) exposes its
positional overload; cmd_system_task_monitor(..., follow=False) exposes the
positive side of --once/--follow. cmd_run(describe=True, spec=REF) returns
spec-aware help metadata and never submits.
weft.commands.__all__ exports the 41 functions and these consumption types:
InitResult, RunSpecDescription, RunSession, CommandStream,
RunExecutionResult, SubmittedTaskReceipt, TaskSnapshot, TaskResult,
TaskEvent, ServiceSnapshot, TaskPingResult, TaskControlResult,
TaskControlFailure,
QueueEntry, QueueInfo, QueueWriteReceipt, QueueMoveResult,
QueueDeleteReceipt, QueueBroadcastReceipt, QueueAliasRecord,
EndpointResolution, ManagerSnapshot, SpecRecord,
SpecValidationResult, SpecMutationResult, SystemStatusSnapshot,
SystemTidyResult, SystemLoadResult, SystemDumpResult,
SystemPruneResult, BuiltinSpecRecord, TaskMonitorConfig,
TaskMonitorResult, TaskMonitorRecord, and TaskMonitorSummary.
New/refined exact contracts:
InitResult(root: Path, config_path: Path, created: bool).RunSpecDescription(reference: str, usage: str, arguments: tuple[Mapping[str, Any], ...], stdin: Mapping[str, Any] | None).CommandStream[T]is an iterator with idempotentclose(); exhaustion and close release resources and iteration failures use typed command errors. Broker-backed stream advancement and final closure share an executing-thread owner. Transport adapters preserve that ownership across async delivery and cancellation; they do not close an iterator concurrently with an active advance. A genuine stream-body failure stays primary when cleanup also fails; a cleanup failure during lifecycle-only generator closure remains observable to the close caller.RunSessionexposestid,events() -> CommandStream[TaskEvent],send_input(text),close_input(),stop() -> TaskControlResult,wait(timeout=None) -> RunExecutionResult, and idempotentclose(). Close releases owned resources but does not cancel the task.
Implementation mapping: weft/commands/_resources.py owns stream cleanup
exception priority; weft/commands/_result_wait.py, _spawn_submission.py,
events.py, queue.py, result.py, system.py, and tasks.py own the
bounded and generator observation scopes; weft/commands/run.py::_LiveRunSession
owns input, direct cancellation wake, and child-stream closure on the thread
that advances each iterator. It shares the plain callback registration helper
weft/helpers/__init__.py::_register_observation_wakeup with Django.
Cross-thread Django adaptation is mapped
under [DJ-12.1] and [DJ-12.2]. Firing cleanup coverage lives in
tests/commands/test_resource_cleanup.py, test_queue.py, test_status.py,
and integrations/weft_django/tests/test_weft_django.py.
QueueMoveResult(source: str, destination: str, entries: tuple[QueueEntry, ...], moved_count: int); entries are the exact ordered moved set.TaskResultretains its result fields and carries optionalreconciliation: dict[str, Any] | None = Noneso the CLI can render the existing claimed-result metadata contract [CLI-1.2.2]. The owner isweft/commands/result.py::await_task_result; the CLI renders that evidence without another lifecycle probe. Implementation plan:2026-08-31-dead-generation-retirement-plan(retired — source 396c3b85; seedocs/plans/README.md).TaskPingResult(tid: str, acknowledged: bool, timed_out: bool, error: str | None, observed_at: int | None, pong: Mapping[str, Any] | None, snapshot: TaskSnapshot | None). A synchronous keyed PING owns an ephemeral reply queue whose lifetime is bounded by the dead-task cleanup age. Its timeout must be finite and satisfy0 <= timeout <= CONTROL_PING_MAX_TIMEOUT_SECONDS. A value outside that inclusive range raisesCommandUsageErrorbefore any queue is created; it is not clamped and is distinct from a probe timeout.TaskControlFailure(tid: str, error: str, error_type: str)records a selected task whose control attempt could not be confirmed.erroris the rendered failure message anderror_typeis the exception class name.TaskControlResult(command: Literal["stop", "kill"], requested: tuple[str, ...], accepted: tuple[str, ...], failures: tuple[TaskControlFailure, ...], snapshots: tuple[TaskSnapshot, ...]).acceptedandfailurespartitionrequested. An empty selection is a successful zero-count outcome. In a sweep, the command raises only when at least one task was requested and none was accepted; the raisedControlRejectedcarries the full failure tuple on its documentedfailuresattribute. A single genuinely unknown TID instead propagatesTaskNotFound, which the CLI renders with exit code 2. A known terminal task rejectsstopwithControlRejectedand analready <status>message before a control queue is written. It remains eligible forkillso kill escalation can reap runtime residue; when no live runtime can be proven, the rejection states the terminal status and absence of runtime residue. Clientstop_many()/kill_many()with no selector are the empty-selection case. Explicittidscannot be combined withall_tasksorpattern; mixed scope is a typed usage error.QueueDeleteReceipt(queue: str | None, deleted_count: int, queues_deleted: int, all_queues: bool, exact_message: str | None).SpecMutationResult(action: Literal["create", "delete"], record: SpecRecord).SystemDumpResult(path: Path, queues: int, messages: int, aliases: int, omitted_claimed_queues: int, omitted_claimed_messages: int).SystemPruneResult(families: tuple[str, ...], applied: bool, candidates: int, deleted: int, failed: int, details: Mapping[str, Any]).BuiltinSpecRecord(name: str, description: str | None, category: str | None, function_target: str | None, supported_platforms: tuple[str, ...], path: Path, source: str = "builtin").TaskMonitorRecord(record: Mapping[str, Any])is the lossless pre-serialization form of each run-start, task-summary, and run-completed record in exact emission order.TaskMonitorResult(log_path: Path | None, records_written: int, events_scanned: int, tids_seen: int, summaries_emitted: int, checkpoint_timestamp: int | None, records: tuple[TaskMonitorRecord, ...]). It has no exit-code, stdout, stderr, or JSON-rendering fields.TaskMonitorConfig(context: str | Path | None = None, follow: bool = False, sink: Literal["stdout", "disk"] = "stdout", log_dir: Path | None = None, checkpoint: Path | None = None, no_checkpoint: bool = False, since: int | None = None, limit: int | None = None, monitor_name: str = "default")andTaskMonitorSummary(record: Mapping[str, Any]).RunExecutionResulthas nosubmission_error; terminal status, result, and failure detail remain outcome data.SpecRecordhaspayload: Mapping[str, Any] | None = None; show always supplies it.TaskEvent.payloadlosslessly carries lifecycle, output chunk, result, and control envelopes in observed order.TaskSnapshotaddshost_pids,managed_pids, andlive_managed_pids, eachtuple[int, ...] | None = None; process mode populates them.ManagerSnapshotaddsliveness: Literal["live", "stale", "unknown", "non_live"] | None = None,proof_source: str | None = None,proof_detail: str | None = None,dispatch_eligible: bool | None = None,canonical_candidate: bool | None = None, andcanonical: bool | None = None; diagnostic mode populates them.started_here: bool | None = Noneis populated only bycmd_manager_startso the CLI can preserve its started-versus-existing lifecycle message without a second semantic query.
| Family | Return |
|---|---|
| root init | InitResult |
| root status | SystemStatusSnapshot, or CommandStream[TaskEvent] iff watch |
| root result | TaskResult, tuple in all mode, or CommandStream[TaskEvent] iff stream |
| root run | RunSpecDescription iff describe; otherwise RunSession iff wait; otherwise RunExecutionResult |
| queue read/peek/watch | tuple/stream of QueueEntry |
| queue write/move/delete/broadcast | corresponding receipt/result |
| queue list/exists/stats/resolve/alias | named QueueInfo, EndpointResolution, QueueAliasRecord, tuple, or bool |
| spec create/delete/list/show/validate/generate | mutation, record tuple/record, validation, or mapping as named |
| task list/status/ping/stop/kill/tid | snapshots/events, ping/control result, or full TID |
| manager start/status/list | snapshot or tuple; serve blocks and returns None |
| manager stop | terminal snapshot, or None when already absent |
| system tidy/load/task-monitor/prune/dump/builtins | corresponding named outcome |
Interactive run only changes session capabilities. Task monitor returns
CommandStream[TaskMonitorSummary] iff follow=True, else
TaskMonitorResult; sink does not alter the branch. cmd_manager_serve is the
sole blocking non-stream exception and emits no process output.
Client system dump, load, and tidy preserve the supplied resolved
context, including its broker target and artifact directory. Without an
explicit output, the export is weft_export.jsonl in that context's
Weft directory; without an explicit input, load reads that same file.
A RunSession.stop likewise controls the task on the context the
session was submitted with.
Implementation: weft/commands/dump.py::dump_system and cmd_system_dump
share the resolved-context materializer; weft/commands/load.py and
weft/commands/tidy.py share theirs the same way, and
weft/commands/run.py::_LiveRunSession.stop passes its session context
to the task-control owner.
Correction plan: 2026-09-08-complexity-review-corrections-plan (retired — source 396c3b85; see docs/plans/README.md).
The exact hierarchy is CommandError(WeftError),
CommandUsageError(CommandError, ValueError),
CommandTimeoutError(CommandError, TimeoutError), and
CommandExecutionError(CommandError, RuntimeError). Existing InvalidTID,
TaskNotFound, SpecNotFound, ControlRejected, ManagerNotRunning, and
ManagerStartFailed retain their inheritance. Submission errors are
SubmissionError(CommandError), SubmissionValidationError(SubmissionError),
and SubmissionManagerError(SubmissionError). Backend, validation, and OS
errors are translated with chaining at the command seam. Error classes
exported by client and commands are identical objects.
The CLI maps CommandUsageError, InvalidTID, TaskNotFound, and
SpecNotFound to 2; CommandTimeoutError to 124; every other CommandError
to 1 except the spec-pinned system load alias-conflict case, which returns 3
before writes begin; success maps to 0. When an internal remaining-budget or
completion-grace wait expires, the public timeout diagnostic retains the
caller's requested timeout rather than exposing the internal sub-budget.
Ctrl-C remains a shell concern.
WeftClient.submit_spec(reference, *, spec_args=(), payload=None, stdin_text=None, **overrides) and prepare_spec(...) share the run pipeline:
parameterization first, remaining tokens to run-input, before TID commit. For
a spec declaring run-input, adapter output is the payload and payload= is
rejected. Without run-input, payload= is valid. cmd_run and client spec
submission accept a single stdin_text: str | None. The submission seam routes
it to declared run-input stdin when the spec's run_input declares stdin, as
the initial work payload when the spec has no run_input contract, and rejects
it with a typed usage error when a run_input contract exists but declares no
stdin. Client and command surfaces never read process stdin; the CLI adapter
reads piped stdin once and forwards it as stdin_text. Failures are typed; a
returned Task is the committed receipt and uses the materialized spec's
runtime context. Runtime roots expand ~ and resolve to an absolute path
through the shared submission seam on every surface. Declared-argument parse
errors are CommandUsageError; malformed/materialization and adapter failures
are SubmissionValidationError with the original exception chained, except
that an adapter-raised WeftError retains its exact public type and mapping. A
non-persistent human-readable name is not an endpoint claim and therefore
does not use endpoint-name syntax validation; persistent names still do.
payload= and stdin_text= are mutually exclusive even when
the spec has no run_input contract, so the initial work payload has one owner.
Submit-override semantics are identical on every surface that accepts
**overrides: an override whose value is None is ignored and the template's
value stands; a name outside the shared public override vocabulary raises
TypeError from prepare(...), prepare_pipeline(...), and
submit_command(...), and SubmissionValidationError from prepare_spec(...);
a value the TaskSpec schema rejects raises the schema's validation error (a
ValueError) from prepare(...) and SubmissionValidationError from
prepare_spec(...); a name in the reserved _weft. namespace raises
ValueError. Each submit(...), submit_spec(...), and submit_pipeline(...)
raises exactly what its prepare* counterpart raises.
weft.client.normalize_taskspec_payload(taskspec, **overrides) runs the shared
definition-normalization contract without a client or context and returns the validated,
normalized TaskSpec definition as a fresh JSON-compatible dict: the definition
snapshot used by prepare(...) before runtime-context binding, submission-time
transport encoding, or reserved-metadata handling. Overrides are applied, re-validated,
and JSON round-tripped. Pure normalization preserves a relative or home-relative
spec.weft_context declaration as text. It accepts a TaskSpec or a
JSON-compatible mapping (a mapping without tid is validated as a template),
raises the errors from prepare(...)'s shared definition-normalization stage, constructs no Weft context, reads no
configuration, resolves no project root, opens no broker, and writes nothing.
Every keyword argument is an override name: payload is not part of the
override vocabulary and raises TypeError here. The mapping carries no
top-level bundle-root marker and no bundle provenance; a TaskSpec input that
already carries a bundle root is accepted, its root is re-resolved read-only by
TaskSpec validation, and the result drops it. Embedders that need a TaskSpec
definition for composition rather than submission call it; there is no second
normalization path.
Context-aware prepare(), prepare_spec(), and prepare_pipeline() bind a nonempty
declared spec.weft_context to an absolute path using the preparation-time CWD and home
expansion. Binding occurs after parameterization and overrides and before any run-input
adapter observes the runtime root. An absent declaration stays unset and uses the
captured client's root at submission. Binding copies and validates the TaskSpec without
mutating its input, TID, io, bundle provenance, or request flags. It opens no
alternate broker; broker selection for an explicit alternate root remains at submission
using captured Config. Pure normalize_taskspec_payload() does not perform
runtime-context binding.
Implementation mapping: weft/client/_client.py::normalize_taskspec_payload over
weft/commands/submission.py::prepare_definition. Runtime context binding is owned by
weft/commands/submission.py::_bind_declared_context,
weft/commands/submission.py::prepare, weft/commands/submission.py::prepare_spec,
and weft/commands/submission.py::prepare_pipeline. Related preparation input and
error contracts are covered by weft/ext.py, tests/core/test_client.py, and
tests/commands/test_submission.py.
Resource ownership rule for [PY-3]:
weft/commands/submission.py::_submit_prepared_outcome(..., session=None) owns
one bounded session when no session is supplied and borrows the supplied
session otherwise. Both paths open one bounded connection operation for the
committed spawn write and immediate manager observation, capture the accepted
TID before operation exit, and release the operation before manager recovery.
Submission never closes or recycles a borrowed session. Operation, session, or
later recovery failures retain the accepted-TID annotation.
PreparedSubmission.submit() asks its owning client to select the retained or
bounded path. Within an active same-context scope it borrows that client's
session. After scope exit or in a forked child it uses bounded ownership with
its captured context and never borrows a fork-inherited session. Every client
submission method, including submit_command(), uses this seam. A TaskSpec
whose effective runtime root differs from the base client context uses bounded
ownership; clients do not retain a map of alternate-root sessions.
This also serves the Django on-commit adapter.
See 2026-09-17-submission-manager-check-cost-plan (retired — source 396c3b85; see docs/plans/README.md).
Runtime imports are one-way: cli -> commands -> core,
client -> commands -> core, and core -> ext; commands may also import ext.
Commands never import adapters; core never imports commands or adapters; CLI
and client never import each other; runtime ext imports never point back to
core, commands, or adapters. Type-checking-only extension annotations may refer to public client schema
types. The client package initializer may re-export only the declared schema
types from weft.core.taskspec.model; this is a value-type facade exception,
not permission for client operations to call core. CLI and core initializers remain import-light markers; client and
commands are public package facades and ext is a public module. Architecture
tests enforce the graph, facade inventory/laziness, CLI bijection, no command
stdin access, and exactly one matching facade invocation per Typer callback.
-
Measured admission and bounded family scheduling governs the measured-admission and bounded-family implementation.
-
Connection-aware admission and child progress is superseded by primary-source lineage under [PY-1].
-
retired: 2026-09-19-interactive-source-integration-plan — source 396c3b85; see docs/plans/README.md
-
retired: 2026-09-17-manager-discovery-and-durable-submission-plan — source 396c3b85; see docs/plans/README.md
-
retired: 2026-09-14-django-core-context-resolution-plan — source 396c3b85; see docs/plans/README.md
-
retired: 2026-09-11-public-python-contracts-plan — source 396c3b85; see docs/plans/README.md
-
retired: 2026-09-11-per-tid-task-state-namespace-plan — source 396c3b85; see docs/plans/README.md
-
retired: 2026-08-11-python-api-surfaces-sb-contract — source 396c3b85; see docs/plans/README.md
-
retired: 2026-08-12-public-api-surface-remediation — source 396c3b85; see docs/plans/README.md
-
retired: 2026-09-08-django-override-normalization-seam-plan — source 396c3b85; see docs/plans/README.md
Implementation plan: Task-owned observation and read separation.