Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

/**
* An extension that decides whether a stage may run.
*
* <p>
* The gate is the only extension kind that may refuse work. A denial short-circuits
* the stage with a reason; an approval request is handed to the approval channel of
* the engine. An engine without an approval channel must degrade an approval request
* to a denial instead of letting the stage through.
*
* <p>
* The gate does not rewrite anything. A gate that wants to change a request combines
* with a {@link LifecycleTransformer}; a gate that wants to wrap the stage combines
* with an {@link AroundInterceptor}.
*/
public interface AdmissionGate extends LifecycleExtension {

/**
* The stage this gate is bound to.
* @return the bound stage, never null
*/
LifecycleStage stage();

/**
* Decides whether the stage may run.
* @param event the event of the bound stage, never null
* @return the decision, never null
*/
GateDecision admit(LifecycleEvent event);

@Override
default java.util.Set<LifecycleStage> stages() {
return LifecycleExtension.boundTo(stage());
}

@Override
default LifecycleCapabilities capabilities() {
return LifecycleCapabilities.synchronousOnly();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

/**
* The continuation an around interceptor wraps.
*
* <p>
* The chain is the part of the stage that actually performs the work. An interceptor
* may call it zero times to short-circuit the stage, once for the normal path, or more
* than once when the stage is retried.
*/
@FunctionalInterface
public interface AroundChain {

/**
* Continues the stage.
* @param event the event handed to the stage, never null
* @return the event produced by the stage, never null
*/
LifecycleEvent proceed(LifecycleEvent event);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

/**
* An extension that wraps a stage and can rewrite, short-circuit or repeat it.
*
* <p>
* The around interceptor is the strongest extension kind: it may rewrite the event it
* receives, may return a result without calling the chain at all, and may call the
* chain more than once to implement a retry or a fallback.
*
* <p>
* It may not change the trusted identity of the run. The contract does not expose a
* way to rewrite the trusted execution context, and an interceptor that needs a
* stronger identity must ask the run for a new one instead of editing the current one.
*
* <p>
* A failing interceptor aborts the stage. The failure is explicit: the engine reports
* it as a {@link LifecycleEvent.ExtensionFailed} event.
*/
public interface AroundInterceptor extends LifecycleExtension {

/**
* The stage this interceptor is bound to.
* @return the bound stage, never null
*/
LifecycleStage stage();

/**
* Wraps the stage.
* @param event the event of the bound stage, never null
* @param chain the continuation of the stage, never null
* @return the event that leaves the stage, never null
*/
LifecycleEvent intercept(LifecycleEvent event, AroundChain chain);

@Override
default java.util.Set<LifecycleStage> stages() {
return LifecycleExtension.boundTo(stage());
}

@Override
default LifecycleCapabilities capabilities() {
return LifecycleCapabilities.synchronousOnly();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

import java.util.Objects;

/**
* Identity of one attempt inside an invocation.
*
* <p>
* An attempt is a retry of a unit of work. It is distinct from the invocation itself
* and from a step: a retried model call is a new attempt of the same step.
*
* @param value the opaque attempt identifier
*/
public record AttemptId(String value) {

/**
* Creates an attempt identity.
* @param value the opaque attempt identifier, must not be null
*/
public AttemptId {
Objects.requireNonNull(value, "value must not be null");
}

/**
* Creates an attempt identity from its textual form.
* @param value the opaque attempt identifier, must not be null
* @return the attempt identity
*/
public static AttemptId of(String value) {
return new AttemptId(value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

import java.util.Objects;

/**
* Identity of one delegation inside an invocation.
*
* <p>
* A delegation is the hand-over of work to another agent, plus the join of its result.
* It is not a tool call: the delegated agent runs its own invocation with its own
* lifecycle.
*
* @param value the opaque delegation identifier
*/
public record DelegationId(String value) {

/**
* Creates a delegation identity.
* @param value the opaque delegation identifier, must not be null
*/
public DelegationId {
Objects.requireNonNull(value, "value must not be null");
}

/**
* Creates a delegation identity from its textual form.
* @param value the opaque delegation identifier, must not be null
* * @return the delegation identity
*/
public static DelegationId of(String value) {
return new DelegationId(value);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.agentic.ai.agent.lifecycle;

import java.util.Objects;

/**
* The outcome of an admission gate.
*
* <p>
* Three outcomes exist, and they are not interchangeable:
*
* <ul>
* <li>{@link Outcome#ALLOW} — the stage proceeds.</li>
* <li>{@link Outcome#DENY} — the stage is short-circuited with a reason.</li>
* <li>{@link Outcome#REQUIRE_APPROVAL} — the stage needs an approval decision. An
* engine without an approval channel must degrade this to {@link Outcome#DENY} and say
* so, never to {@link Outcome#ALLOW}.</li>
* </ul>
*
* @param outcome the gate outcome, never null
* @param reason the reason of a denial or an approval request, nullable
*/
public record GateDecision(Outcome outcome, String reason) {

/**
* Creates a gate decision.
* @param outcome the gate outcome, must not be null
* @param reason the reason of a denial or an approval request, nullable
*/
public GateDecision {
Objects.requireNonNull(outcome, "outcome must not be null");
if (outcome != Outcome.ALLOW && (reason == null || reason.isBlank())) {
throw new IllegalArgumentException("a reason is required for " + outcome);
}
}

/**
* Allows the stage.
* @return an allowing decision
*/
public static GateDecision allow() {
return new GateDecision(Outcome.ALLOW, null);
}

/**
* Denies the stage with a reason.
* @param reason the denial reason, must not be blank
* @return a denying decision
*/
public static GateDecision deny(String reason) {
return new GateDecision(Outcome.DENY, reason);
}

/**
* Requests an approval decision for the stage.
* @param reason the approval reason, must not be blank
* @return an approval request
*/
public static GateDecision requireApproval(String reason) {
return new GateDecision(Outcome.REQUIRE_APPROVAL, reason);
}

/**
* Whether the stage may proceed.
* @return true for an allowing decision
*/
public boolean isAllowed() {
return outcome == Outcome.ALLOW;
}

/**
* Whether the stage is short-circuited.
* @return true for a denying decision
*/
public boolean isDenied() {
return outcome == Outcome.DENY;
}

/**
* Whether the stage waits for an approval decision.
* @return true for an approval request
*/
public boolean requiresApproval() {
return outcome == Outcome.REQUIRE_APPROVAL;
}

/**
* The outcome of an admission gate.
*/
public enum Outcome {

/**
* The stage proceeds.
*/
ALLOW,

/**
* The stage is short-circuited.
*/
DENY,

/**
* The stage needs an approval decision.
*/
REQUIRE_APPROVAL
}

}
Loading
Loading