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.
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. 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 and WeftClient do
not cache a live session. Existing queue() and broker() ownership and
defaults are unchanged.
Implementation mapping: weft/context.py::WeftContext.session owns the
factory. Exact target/config behavior is covered by
tests/context/test_context.py; real-backend task/session cleanup is covered by
tests/core/test_task_runtime_connections.py.
Related plan: SimpleBroker configuration migration. 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.
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 the shared per-TID runtime-state readers ([OBS.6]); known-TID snapshots read that state queue directly. Snapshot and control waiters subscribe to the exact queue even before its first write; realtime snapshot requests remain fresh. CLI event watches remain incremental event streams. This storage change adds no public signature, result shape, iterator retention contract, or lifecycle precedence change. Python system status retains its unfiltered default; CLI status retains terminal filtering.
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.
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 and child-stream closure. 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: Dead generation retirement.TaskPingResult(tid: str, acknowledged: bool, timed_out: bool, error: str | None, observed_at: int | None, pong: Mapping[str, Any] | None, snapshot: TaskSnapshot | None).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: Complexity review corrections.
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.
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.