001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import org.jspecify.annotations.NonNull;
020
021import javax.annotation.concurrent.ThreadSafe;
022import java.util.Optional;
023
024/**
025 * Thread-safe application authority for MCP task state and client actions.
026 *
027 * <p>A production implementation normally delegates to application-owned
028 * durable storage and job infrastructure shared by every eligible Soklet
029 * node. It owns task-ID generation, persistence, retention, task-to-principal
030 * and tenant binding, legal atomic state transitions, client-input
031 * deduplication, cancelation intent, and publication of already-durable status
032 * changes. Soklet does not close the manager or start, stop, schedule, retry,
033 * lease, or fence application workers.
034 *
035 * <p>Every operation is independently admitted before this interface is
036 * called, but admission alone does not authorize a task. Implementations must
037 * atomically compare the current request's endpoint and admitted application
038 * identity with their durable task binding. Unknown and unauthorized task IDs
039 * must be indistinguishable: {@link #findTask(McpTaskRequestContext)} returns
040 * an empty optional for either case, while mutation methods throw
041 * {@link McpTaskNotFoundException} for either case. Other failures are treated
042 * as internal errors and must not expose exception-derived data to the client.
043 *
044 * <p>A task must be durably readable through {@link #findTask} before its
045 * handler returns {@link McpTaskCreatedResult}. Correct production behavior
046 * cannot depend on the original HTTP connection, sticky routing, or the
047 * originating process remaining alive. Implementations should assume
048 * at-least-once work execution and use leases, fencing, idempotency, and an
049 * outbox or equivalent recovery mechanism where their workload requires them.
050 *
051 * <p>Soklet validates returned snapshots at delivery time. Invalid stored
052 * output or undeclared input requests fail polling with a fixed internal
053 * error and suppress notification projection without mutating task state,
054 * publishing a state change, or notifying an application worker. Current
055 * access policy, sanitization, and transport limits can also prevent delivery
056 * without proving that the underlying work failed.
057 * Applications must validate their output and input requests before storing
058 * them; preserve the framework origin as opaque data rather than interpreting
059 * its persisted members. This interface supplies no worker-side validation
060 * callback.
061 *
062 * @author <a href="https://www.revetkn.com">Mark Allen</a>
063 */
064@ThreadSafe
065public interface McpTaskManager {
066        /**
067         * Creates Soklet's explicit process-local task manager with documented
068         * bounded defaults.
069         *
070         * <p>The returned manager is intended for development, tests, simulation,
071         * and deliberately ephemeral single-process applications. Its state is lost
072         * on restart and is invisible to other nodes. It provides no worker,
073         * executor, queue, outbox, leases, fencing, failover, or crash recovery and
074         * is never selected implicitly by an {@link McpServer}.
075         *
076         * @return a new independent in-memory task manager
077         */
078        @NonNull
079        static McpInMemoryTaskManager fromInMemoryDefaults() {
080                return new McpInMemoryTaskManager();
081        }
082
083        /**
084         * Returns the optional application-owned publisher for durable task-state
085         * changes.
086         * <p>
087         * When present, Soklet subscribes while the server is running and may project
088         * matching events onto {@code subscriptions/listen} streams as
089         * {@code notifications/tasks}. The publisher carries task IDs only; Soklet
090         * performs a fresh, authorized lookup through this manager before sending a
091         * current snapshot. Polling through {@code tasks/get} remains authoritative.
092         * <p>
093         * This is a stable manager property: every invocation for the manager's
094         * lifetime must return either an empty optional or an optional containing the
095         * same publisher instance. Each built {@link McpServer} snapshots the property
096         * once during construction; it does not observe a later publisher change.
097         * This lets production and simulator servers backed by the same manager attach
098         * independently to one shared event topology.
099         * <p>
100         * The default returns an empty optional, enabling polling without task
101         * notifications. Soklet neither starts nor closes the publisher.
102         *
103         * @return task-event publisher, or the empty optional when task notifications
104         * are disabled
105         */
106        @NonNull
107        default Optional<@NonNull McpTaskEventPublisher> getTaskEventPublisher() {
108                return Optional.empty();
109        }
110
111        /**
112         * Finds the current authoritative task snapshot after atomically
113         * authorizing the request.
114         * <p>
115         * Soklet may invoke this method asynchronously to project a task-change event.
116         * A subscription can disconnect while that lookup is in progress, so
117         * implementations should apply finite storage deadlines and respond promptly
118         * to thread interruption; a result may be discarded when the subscription is
119         * no longer active.
120         * <p>
121         * After an authorized lookup, Soklet checks outstanding input requests
122         * against the polling request's current client capabilities. Missing support
123         * fails that poll with the missing-capability protocol error, without sending
124         * a partial snapshot or changing authoritative state. Working and terminal
125         * snapshots do not require capabilities for unused input declarations.
126         *
127         * @param taskRequestContext independently admitted lookup context
128         * @return current snapshot, or empty when the task is unknown or unauthorized
129         * @throws Exception if task storage or authorization fails unexpectedly
130         */
131        @NonNull
132        Optional<@NonNull McpTask> findTask(
133                        @NonNull McpTaskRequestContext taskRequestContext) throws Exception;
134
135        /**
136         * Atomically authorizes and accepts client responses to outstanding task
137         * input requests.
138         *
139         * <p>A request may supply any partial subset of outstanding responses.
140         * Unknown, already-consumed, and superseded response keys are ignored, as is
141         * a response whose MCP union variant does not match its outstanding request.
142         * Implementations may perform that correlation through
143         * {@link McpInputRequest#matchesInputResponse(McpJsonValue)}.
144         * The update acknowledgement may precede a worker's subsequent observable
145         * state change.
146         *
147         * @param taskUpdateContext independently admitted task update
148         * @throws McpTaskNotFoundException if the task is unknown or unauthorized
149         * @throws Exception if task storage, authorization, or update processing
150         * fails unexpectedly
151         */
152        void updateTask(@NonNull McpTaskUpdateContext taskUpdateContext) throws Exception;
153
154        /**
155         * Atomically authorizes and records a client's durable task-cancelation
156         * intent.
157         *
158         * <p>A successful return acknowledges intent only. A worker may later honor
159         * it, or completion or failure may win the race. This method is unrelated to
160         * request-scoped {@link CancelationToken} and
161         * {@code notifications/cancelled} behavior. A known terminal task must be
162         * accepted idempotently; only an unknown or unauthorized task uses the
163         * not-found signal.
164         *
165         * @param taskRequestContext independently admitted task-cancelation request
166         * @throws McpTaskNotFoundException if the task is unknown or unauthorized
167         * @throws Exception if task storage, authorization, or intent recording
168         * fails unexpectedly
169         */
170        void requestTaskCancelation(
171                        @NonNull McpTaskRequestContext taskRequestContext) throws Exception;
172}