diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AdmissionGate.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AdmissionGate.java new file mode 100644 index 000000000..8f071902b --- /dev/null +++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AdmissionGate.java @@ -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. + * + *
+ * 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. + * + *
+ * 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
+ * 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);
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AroundInterceptor.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AroundInterceptor.java
new file mode 100644
index 000000000..97c3f40a2
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/AroundInterceptor.java
@@ -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.
+ *
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * 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
+ * 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);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/DelegationId.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/DelegationId.java
new file mode 100644
index 000000000..456a0fa8b
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/DelegationId.java
@@ -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.
+ *
+ *
+ * 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);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/GateDecision.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/GateDecision.java
new file mode 100644
index 000000000..3af674324
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/GateDecision.java
@@ -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.
+ *
+ *
+ * Three outcomes exist, and they are not interchangeable:
+ *
+ *
+ * An invocation is the outermost unit of work: one call into an agent. Every attempt,
+ * step, model turn, tool call and delegation belongs to exactly one invocation.
+ *
+ * @param value the opaque invocation identifier
+ */
+public record InvocationId(String value) {
+
+ /**
+ * Creates an invocation identity.
+ * @param value the opaque invocation identifier, must not be null
+ */
+ public InvocationId {
+ Objects.requireNonNull(value, "value must not be null");
+ }
+
+ /**
+ * Creates an invocation identity from its textual form.
+ * @param value the opaque invocation identifier, must not be null
+ * @return the invocation identity
+ */
+ public static InvocationId of(String value) {
+ return new InvocationId(value);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleCapabilities.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleCapabilities.java
new file mode 100644
index 000000000..205d703d7
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleCapabilities.java
@@ -0,0 +1,68 @@
+/*
+ * 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;
+
+/**
+ * Capability flags declared by a lifecycle extension.
+ *
+ *
+ * A capability answers a single question: does the extension support this execution
+ * mode at its bound stage? The flags are descriptive, never a promise that every engine
+ * provides every stage. An engine that cannot honour a flag must say so instead of
+ * silently degrading the extension.
+ *
+ * @param synchronous whether the extension runs on the calling thread
+ * @param asynchronous whether the extension may complete on another thread
+ * @param cancellable whether the extension observes cancellation of the bound stage
+ * @param streaming whether the extension participates in a streamed stage
+ * @param resumable whether the extension survives a suspend and resume of the bound
+ * stage
+ */
+public record LifecycleCapabilities(boolean synchronous, boolean asynchronous, boolean cancellable, boolean streaming,
+ boolean resumable) {
+
+ /**
+ * No capability at all.
+ * @return capabilities without any supported execution mode
+ */
+ public static LifecycleCapabilities none() {
+ return new LifecycleCapabilities(false, false, false, false, false);
+ }
+
+ /**
+ * The extension only runs synchronously on the calling thread.
+ * @return synchronous-only capabilities
+ */
+ public static LifecycleCapabilities synchronousOnly() {
+ return new LifecycleCapabilities(true, false, false, false, false);
+ }
+
+ /**
+ * The extension only completes asynchronously.
+ * @return asynchronous-only capabilities
+ */
+ public static LifecycleCapabilities asynchronousOnly() {
+ return new LifecycleCapabilities(false, true, false, false, false);
+ }
+
+ /**
+ * Whether the extension supports at least one execution mode.
+ * @return true when any capability is declared
+ */
+ public boolean supportsAny() {
+ return synchronous || asynchronous || cancellable || streaming || resumable;
+ }
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleEvent.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleEvent.java
new file mode 100644
index 000000000..41959bc4b
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleEvent.java
@@ -0,0 +1,261 @@
+/*
+ * 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 immutable notification about one lifecycle stage.
+ *
+ *
+ * The event set is closed on purpose: a plugin never receives a free-form payload map
+ * it has to interpret, and it never receives a mutable object it could corrupt for
+ * everybody else. Every event carries the stage it belongs to and the identity of the
+ * work it describes.
+ *
+ *
+ * Events are observations, not commands. Mutating an event is not possible; the only
+ * way to influence a stage is the transformer, the around interceptor or the admission
+ * gate.
+ */
+public sealed interface LifecycleEvent permits LifecycleEvent.StageEntered, LifecycleEvent.StageExited,
+ LifecycleEvent.ModelCallRequested, LifecycleEvent.ModelCallCompleted, LifecycleEvent.ToolCallRequested,
+ LifecycleEvent.ToolCallCompleted, LifecycleEvent.ToolCallRejected, LifecycleEvent.ExtensionFailed {
+
+ /**
+ * The stage this event belongs to.
+ * @return the stage, never null
+ */
+ LifecycleStage stage();
+
+ /**
+ * The identity of the work this event describes.
+ * @return the identity, never null
+ */
+ LifecycleIdentity identity();
+
+ /**
+ * A stage was entered.
+ *
+ * @param stage the stage, never null
+ * @param identity the identity of the work, never null
+ */
+ record StageEntered(LifecycleStage stage, LifecycleIdentity identity) implements LifecycleEvent {
+
+ /**
+ * Creates a stage-entered event.
+ * @param stage the stage, must not be null
+ * @param identity the identity of the work, must not be null
+ */
+ public StageEntered {
+ java.util.Objects.requireNonNull(stage, "stage must not be null");
+ java.util.Objects.requireNonNull(identity, "identity must not be null");
+ }
+ }
+
+ /**
+ * A stage was left.
+ *
+ * @param stage the stage, never null
+ * @param identity the identity of the work, never null
+ * @param successful whether the stage completed successfully
+ * @param failureReason the failure description, nullable
+ */
+ record StageExited(LifecycleStage stage, LifecycleIdentity identity, boolean successful, String failureReason)
+ implements LifecycleEvent {
+
+ /**
+ * Creates a stage-exited event.
+ * @param stage the stage, must not be null
+ * @param identity the identity of the work, must not be null
+ * @param successful whether the stage completed successfully
+ * @param failureReason the failure description, nullable
+ */
+ public StageExited {
+ java.util.Objects.requireNonNull(stage, "stage must not be null");
+ java.util.Objects.requireNonNull(identity, "identity must not be null");
+ }
+ }
+
+ /**
+ * A model call was requested.
+ *
+ * @param invocation the model call, never null
+ */
+ record ModelCallRequested(ModelCallInvocation invocation) implements LifecycleEvent {
+
+ /**
+ * Creates a model-call-requested event.
+ * @param invocation the model call, must not be null
+ */
+ public ModelCallRequested {
+ java.util.Objects.requireNonNull(invocation, "invocation must not be null");
+ }
+
+ @Override
+ public LifecycleStage stage() {
+ return LifecycleStage.MODEL_CALL;
+ }
+
+ @Override
+ public LifecycleIdentity identity() {
+ return invocation.identity();
+ }
+ }
+
+ /**
+ * A model call was completed.
+ *
+ * @param invocation the model call, never null
+ * @param outcome the outcome of the call, never null
+ */
+ record ModelCallCompleted(ModelCallInvocation invocation, ModelCallOutcome outcome) implements LifecycleEvent {
+
+ /**
+ * Creates a model-call-completed event.
+ * @param invocation the model call, must not be null
+ * @param outcome the outcome of the call, must not be null
+ */
+ public ModelCallCompleted {
+ java.util.Objects.requireNonNull(invocation, "invocation must not be null");
+ java.util.Objects.requireNonNull(outcome, "outcome must not be null");
+ }
+
+ @Override
+ public LifecycleStage stage() {
+ return LifecycleStage.MODEL_CALL;
+ }
+
+ @Override
+ public LifecycleIdentity identity() {
+ return invocation.identity();
+ }
+ }
+
+ /**
+ * A tool call was requested.
+ *
+ * @param invocation the tool call, never null
+ */
+ record ToolCallRequested(ToolCallInvocation invocation) implements LifecycleEvent {
+
+ /**
+ * Creates a tool-call-requested event.
+ * @param invocation the tool call, must not be null
+ */
+ public ToolCallRequested {
+ java.util.Objects.requireNonNull(invocation, "invocation must not be null");
+ }
+
+ @Override
+ public LifecycleStage stage() {
+ return LifecycleStage.TOOL_CALL;
+ }
+
+ @Override
+ public LifecycleIdentity identity() {
+ return invocation.identity();
+ }
+ }
+
+ /**
+ * A tool call was completed.
+ *
+ * @param invocation the tool call, never null
+ * @param outcome the outcome of the call, never null
+ */
+ record ToolCallCompleted(ToolCallInvocation invocation, ToolCallOutcome outcome) implements LifecycleEvent {
+
+ /**
+ * Creates a tool-call-completed event.
+ * @param invocation the tool call, must not be null
+ * @param outcome the outcome of the call, must not be null
+ */
+ public ToolCallCompleted {
+ java.util.Objects.requireNonNull(invocation, "invocation must not be null");
+ java.util.Objects.requireNonNull(outcome, "outcome must not be null");
+ }
+
+ @Override
+ public LifecycleStage stage() {
+ return LifecycleStage.TOOL_CALL;
+ }
+
+ @Override
+ public LifecycleIdentity identity() {
+ return invocation.identity();
+ }
+ }
+
+ /**
+ * A tool call was rejected before execution.
+ *
+ * @param invocation the rejected tool call, never null
+ * @param reason the rejection reason, never null
+ */
+ record ToolCallRejected(ToolCallInvocation invocation, String reason) implements LifecycleEvent {
+
+ /**
+ * Creates a tool-call-rejected event.
+ * @param invocation the rejected tool call, must not be null
+ * @param reason the rejection reason, must not be null
+ */
+ public ToolCallRejected {
+ java.util.Objects.requireNonNull(invocation, "invocation must not be null");
+ java.util.Objects.requireNonNull(reason, "reason must not be null");
+ }
+
+ @Override
+ public LifecycleStage stage() {
+ return LifecycleStage.TOOL_CALL;
+ }
+
+ @Override
+ public LifecycleIdentity identity() {
+ return invocation.identity();
+ }
+ }
+
+ /**
+ * An extension failed while it was invoked.
+ *
+ *
+ * The event is the explicit failure semantics of the contract: an observer failure
+ * is reported instead of being swallowed, and an around interceptor failure is
+ * surfaced as an event rather than as a silently degraded stage.
+ *
+ * @param stage the stage the extension was bound to, never null
+ * @param identity the identity of the work, never null
+ * @param extensionId the identifier of the failing extension, never null
+ * @param message the failure description, never null
+ */
+ record ExtensionFailed(LifecycleStage stage, LifecycleIdentity identity, String extensionId, String message)
+ implements LifecycleEvent {
+
+ /**
+ * Creates an extension-failed event.
+ * @param stage the stage the extension was bound to, must not be null
+ * @param identity the identity of the work, must not be null
+ * @param extensionId the identifier of the failing extension, must not be null
+ * @param message the failure description, must not be null
+ */
+ public ExtensionFailed {
+ java.util.Objects.requireNonNull(stage, "stage must not be null");
+ java.util.Objects.requireNonNull(identity, "identity must not be null");
+ java.util.Objects.requireNonNull(extensionId, "extensionId must not be null");
+ java.util.Objects.requireNonNull(message, "message must not be null");
+ }
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleExtension.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleExtension.java
new file mode 100644
index 000000000..d04ebfe64
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleExtension.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+import java.util.Set;
+
+/**
+ * The root contract of a lifecycle extension.
+ *
+ *
+ * Every extension declares a stable identifier, the stages it binds to and the
+ * execution modes it supports. The declaration is what an engine uses to reject an
+ * extension it cannot honour: an engine that cannot run a bound stage asynchronously
+ * must say so instead of quietly changing the semantics of the extension.
+ */
+public interface LifecycleExtension {
+
+ /**
+ * The stable identifier of the extension.
+ * @return the extension identifier, never null
+ */
+ String id();
+
+ /**
+ * The stages this extension binds to.
+ * @return the bound stages, never null and never empty
+ */
+ Set
+ * The invocation identity is always present. The remaining identities are present only
+ * when the bound stage actually produces them, which keeps the vocabulary explicit
+ * instead of collapsing everything into one opaque identifier:
+ *
+ *
+ * The identity is read-only for extensions: no plugin may rewrite it, and no plugin may
+ * derive a stronger identity than the one it was given.
+ *
+ * @param invocation the invocation identity, never null
+ * @param attempt the attempt identity, nullable
+ * @param step the step identity, nullable
+ * @param modelTurn the model turn identity, nullable
+ * @param toolCall the tool call identity, nullable
+ * @param delegation the delegation identity, nullable
+ */
+public record LifecycleIdentity(InvocationId invocation, AttemptId attempt, StepId step, ModelTurnId modelTurn,
+ ToolCallId toolCall, DelegationId delegation) {
+
+ /**
+ * Creates an identity.
+ * @param invocation the invocation identity, must not be null
+ * @param attempt the attempt identity, nullable
+ * @param step the step identity, nullable
+ * @param modelTurn the model turn identity, nullable
+ * @param toolCall the tool call identity, nullable
+ * @param delegation the delegation identity, nullable
+ */
+ public LifecycleIdentity {
+ Objects.requireNonNull(invocation, "invocation must not be null");
+ }
+
+ /**
+ * Starts a builder for an identity rooted at the given invocation.
+ * @param invocation the invocation identity, must not be null
+ * @return a new builder
+ */
+ public static Builder builder(InvocationId invocation) {
+ return new Builder(invocation);
+ }
+
+ /**
+ * Builder for {@link LifecycleIdentity}.
+ */
+ public static final class Builder {
+
+ private final InvocationId invocation;
+
+ private AttemptId attempt;
+
+ private StepId step;
+
+ private ModelTurnId modelTurn;
+
+ private ToolCallId toolCall;
+
+ private DelegationId delegation;
+
+ private Builder(InvocationId invocation) {
+ this.invocation = Objects.requireNonNull(invocation, "invocation must not be null");
+ }
+
+ /**
+ * Sets the attempt identity.
+ * @param attempt the attempt identity, nullable
+ * @return this builder
+ */
+ public Builder attempt(AttemptId attempt) {
+ this.attempt = attempt;
+ return this;
+ }
+
+ /**
+ * Sets the step identity.
+ * @param step the step identity, nullable
+ * @return this builder
+ */
+ public Builder step(StepId step) {
+ this.step = step;
+ return this;
+ }
+
+ /**
+ * Sets the model turn identity.
+ * @param modelTurn the model turn identity, nullable
+ * @return this builder
+ */
+ public Builder modelTurn(ModelTurnId modelTurn) {
+ this.modelTurn = modelTurn;
+ return this;
+ }
+
+ /**
+ * Sets the tool call identity.
+ * @param toolCall the tool call identity, nullable
+ * @return this builder
+ */
+ public Builder toolCall(ToolCallId toolCall) {
+ this.toolCall = toolCall;
+ return this;
+ }
+
+ /**
+ * Sets the delegation identity.
+ * @param delegation the delegation identity, nullable
+ * @return this builder
+ */
+ public Builder delegation(DelegationId delegation) {
+ this.delegation = delegation;
+ return this;
+ }
+
+ /**
+ * Builds the identity.
+ * @return the immutable identity
+ */
+ public LifecycleIdentity build() {
+ return new LifecycleIdentity(invocation, attempt, step, modelTurn, toolCall, delegation);
+ }
+
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleMutationGuard.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleMutationGuard.java
new file mode 100644
index 000000000..ce162188e
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleMutationGuard.java
@@ -0,0 +1,91 @@
+/*
+ * 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.Optional;
+
+/**
+ * Decides whether a restricted transformation stays inside its boundary.
+ *
+ *
+ * The guard is the executable form of the "restricted transformer" rule. It compares
+ * the original event with the rewritten one and reports the first violation:
+ *
+ *
+ * Renaming a tool is deliberately not a transformation: it would silently redirect the
+ * call to a different binding, which is an admission decision.
+ */
+public final class LifecycleMutationGuard {
+
+ private LifecycleMutationGuard() {
+ }
+
+ /**
+ * Checks a rewritten event against the original one.
+ * @param original the original event, must not be null
+ * @param rewritten the rewritten event, must not be null
+ * @return the violation description, or empty when the rewrite is allowed
+ */
+ public static Optional
+ * The observer is the weakest extension kind and therefore the easiest to adopt. It
+ * cannot rewrite a request, cannot short-circuit a stage and cannot veto anything.
+ *
+ *
+ * A failing observer must not break the run. The contract makes the failure explicit
+ * instead of hiding it: the engine reports it as a
+ * {@link LifecycleEvent.ExtensionFailed} event and continues.
+ */
+public interface LifecycleObserver extends LifecycleExtension {
+
+ /**
+ * Observes one lifecycle event.
+ * @param event the event, never null
+ */
+ void onEvent(LifecycleEvent event);
+
+ /**
+ * The stages this observer is bound to.
+ * @return the bound stages, never null
+ */
+ @Override
+ Set
+ * The catalog distinguishes two scopes:
+ *
+ *
+ * Two boundaries are deliberate. A workflow step is not a model turn: a workflow engine
+ * must not fabricate model turns to satisfy the catalog. A remote agent is not an
+ * in-process tool caller: the neutral contract only exposes the remote agent's own
+ * public run-level stages, never the internals of its tool execution.
+ */
+public enum LifecycleStage {
+
+ /**
+ * Definition validation and creation.
+ */
+ DEFINITION(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly()),
+
+ /**
+ * Admission of a run before any work is scheduled.
+ */
+ RUN_ADMISSION(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly()),
+
+ /**
+ * Start of an admitted run.
+ */
+ RUN_START(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly()),
+
+ /**
+ * Construction of the execution context of a run.
+ */
+ CONTEXT_BUILD(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly()),
+
+ /**
+ * A single model call.
+ */
+ MODEL_CALL(LifecycleScope.COMMON_RUNTIME,
+ new LifecycleCapabilities(true, true, true, false, false)),
+
+ /**
+ * A streamed model call.
+ */
+ MODEL_STREAM(LifecycleScope.COMMON_RUNTIME,
+ new LifecycleCapabilities(false, true, true, true, false)),
+
+ /**
+ * The decision of which action to take next.
+ */
+ ACTION_DECISION(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly()),
+
+ /**
+ * A single tool call.
+ */
+ TOOL_CALL(LifecycleScope.COMMON_RUNTIME, new LifecycleCapabilities(true, true, true, false, false)),
+
+ /**
+ * Delegation, handover and join of work.
+ */
+ DELEGATION(LifecycleScope.COMMON_RUNTIME, new LifecycleCapabilities(true, true, false, false, false)),
+
+ /**
+ * A workflow graph step. Engine specific: a workflow engine owns this stage.
+ */
+ WORKFLOW_STEP(LifecycleScope.ENGINE_SPECIFIC,
+ new LifecycleCapabilities(true, true, true, false, true)),
+
+ /**
+ * Suspension and resumption of a run. Engine specific: the owning engine defines
+ * the resume protocol.
+ */
+ SUSPEND_RESUME(LifecycleScope.ENGINE_SPECIFIC,
+ new LifecycleCapabilities(false, true, false, false, true)),
+
+ /**
+ * Delivery of the run output to the caller.
+ */
+ OUTPUT_DELIVERY(LifecycleScope.COMMON_RUNTIME, new LifecycleCapabilities(true, true, false, false, false)),
+
+ /**
+ * Completion, failure, cancellation and release of a run.
+ */
+ TERMINATION(LifecycleScope.COMMON_RUNTIME, LifecycleCapabilities.synchronousOnly());
+
+ private final LifecycleScope scope;
+
+ private final LifecycleCapabilities capabilities;
+
+ LifecycleStage(LifecycleScope scope, LifecycleCapabilities capabilities) {
+ this.scope = scope;
+ this.capabilities = capabilities;
+ }
+
+ /**
+ * The scope of this stage.
+ * @return the stage scope, never null
+ */
+ public LifecycleScope scope() {
+ return scope;
+ }
+
+ /**
+ * The execution modes this stage supports.
+ * @return the stage capabilities, never null
+ */
+ public LifecycleCapabilities capabilities() {
+ return capabilities;
+ }
+
+ /**
+ * Whether the stage is common to all engines.
+ * @return true for a common runtime stage
+ */
+ public boolean isCommonRuntime() {
+ return scope == LifecycleScope.COMMON_RUNTIME;
+ }
+
+ /**
+ * Whether the stage belongs to a specific engine family.
+ * @return true for an engine specific stage
+ */
+ public boolean isEngineSpecific() {
+ return scope == LifecycleScope.ENGINE_SPECIFIC;
+ }
+
+ /**
+ * The scope of a lifecycle stage.
+ */
+ public enum LifecycleScope {
+
+ /**
+ * A stage every engine is expected to expose.
+ */
+ COMMON_RUNTIME,
+
+ /**
+ * A stage only a particular engine family can offer.
+ */
+ ENGINE_SPECIFIC
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleTransformer.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleTransformer.java
new file mode 100644
index 000000000..32b0044e4
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/LifecycleTransformer.java
@@ -0,0 +1,60 @@
+/*
+ * 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.Optional;
+
+/**
+ * An extension that may rewrite a stage in a restricted way.
+ *
+ *
+ * The transformer receives the event of its bound stage and returns either a rewritten
+ * event or {@link Optional#empty()} to keep the original. It cannot short-circuit the
+ * stage: refusing work is an admission decision and belongs to the
+ * {@link AdmissionGate}.
+ *
+ *
+ * The rewrite is restricted to the fields the event type declares as mutable.
+ * {@link LifecycleMutationGuard} decides whether a rewrite stays inside that boundary;
+ * a rewrite that changes the identity, the tool name or the model turn identity is
+ * rejected.
+ */
+public interface LifecycleTransformer extends LifecycleExtension {
+
+ /**
+ * The stage this transformer is bound to.
+ * @return the bound stage, never null
+ */
+ LifecycleStage stage();
+
+ /**
+ * Rewrites the event of the bound stage.
+ * @param event the event to rewrite, never null
+ * @return the rewritten event, or empty to keep the original
+ */
+ Optional
+ * The executor is the continuation an around interceptor wraps. It performs the actual
+ * model call and returns its outcome.
+ */
+@FunctionalInterface
+public interface ModelCallExecutor {
+
+ /**
+ * Executes the model call.
+ * @param invocation the model call to execute, never null
+ * @return the outcome of the call, never null
+ */
+ ModelCallOutcome execute(ModelCallInvocation invocation);
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelCallInvocation.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelCallInvocation.java
new file mode 100644
index 000000000..62fd54c95
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelCallInvocation.java
@@ -0,0 +1,125 @@
+/*
+ * 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 io.github.agentic.ai.agent.interceptor.ModelRequest;
+import io.github.agentic.ai.api.chat.ChatMessage;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A model call in the neutral lifecycle contract.
+ *
+ *
+ * The invocation is immutable: a restricted transformer may replace the request, but
+ * neither the identity nor the model turn identity may change. The request keeps the
+ * trusted execution context of the run; the neutral contract never exposes a way to
+ * rewrite that context.
+ *
+ * @param identity the identity of the work this call belongs to, never null
+ * @param modelTurn the model turn identity, never null
+ * @param request the model request, never null
+ */
+public record ModelCallInvocation(LifecycleIdentity identity, ModelTurnId modelTurn, ModelRequest request) {
+
+ /**
+ * Creates a model call invocation.
+ * @param identity the identity of the work this call belongs to, must not be null
+ * @param modelTurn the model turn identity, must not be null
+ * @param request the model request, must not be null
+ */
+ public ModelCallInvocation {
+ Objects.requireNonNull(identity, "identity must not be null");
+ Objects.requireNonNull(modelTurn, "modelTurn must not be null");
+ Objects.requireNonNull(request, "request must not be null");
+ request = readOnlySnapshot(request);
+ }
+
+ /**
+ * Returns a genuinely read-only snapshot of the request. Two layers protect the
+ * event: the snapshot is taken from the caller's request, so later changes to the
+ * caller's collections are invisible; and the snapshot's collection accessors are
+ * immutable views, so an extension cannot rewrite what subsequent extensions observe
+ * (clearing the message list or mutating the context map fails). Collection-valued
+ * context and attribute entries are frozen as well, so a nested list or map shared
+ * with the caller cannot become the mutation channel either. Transforming extensions
+ * rebuild a replacement through {@link ModelRequest#builder(ModelRequest)}; the
+ * executor keeps running on the request it holds, so the event never becomes a
+ * mutation channel into dispatch.
+ */
+ private static ModelRequest readOnlySnapshot(ModelRequest request) {
+ Map
+ * A model call either succeeds with a response or fails with an error description.
+ * Exactly one of {@code response} and {@code error} is present.
+ *
+ * @param modelTurn the model turn identity, never null
+ * @param response the model response, nullable
+ * @param error the failure description, nullable
+ */
+public record ModelCallOutcome(ModelTurnId modelTurn, ModelResponse response, String error) {
+
+ /**
+ * Creates a model call outcome.
+ * @param modelTurn the model turn identity, must not be null
+ * @param response the model response, nullable
+ * @param error the failure description, nullable
+ */
+ public ModelCallOutcome {
+ Objects.requireNonNull(modelTurn, "modelTurn must not be null");
+ if ((response == null) == (error == null)) {
+ throw new IllegalArgumentException("exactly one of response and error must be present");
+ }
+ }
+
+ /**
+ * Creates a successful outcome.
+ * @param modelTurn the model turn identity, must not be null
+ * @param response the model response, must not be null
+ * @return the successful outcome
+ */
+ public static ModelCallOutcome success(ModelTurnId modelTurn, ModelResponse response) {
+ return new ModelCallOutcome(modelTurn, response, null);
+ }
+
+ /**
+ * Creates a failed outcome.
+ * @param modelTurn the model turn identity, must not be null
+ * @param error the failure description, must not be null
+ * @return the failed outcome
+ */
+ public static ModelCallOutcome failure(ModelTurnId modelTurn, String error) {
+ return new ModelCallOutcome(modelTurn, null, error);
+ }
+
+ /**
+ * Whether the call succeeded.
+ * @return true when a response is present
+ */
+ public boolean isSuccess() {
+ return response != null;
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelTurnId.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelTurnId.java
new file mode 100644
index 000000000..04b858cbb
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ModelTurnId.java
@@ -0,0 +1,50 @@
+/*
+ * 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 model turn inside an invocation.
+ *
+ *
+ * A model turn is one request to a model and its answer. It is deliberately not the
+ * message identity: {@code AgentEvent.messageId} identifies a message, while a model
+ * turn identifies the model interaction that produced it. A workflow step does not
+ * create a model turn on its own.
+ *
+ * @param value the opaque model turn identifier
+ */
+public record ModelTurnId(String value) {
+
+ /**
+ * Creates a model turn identity.
+ * @param value the opaque model turn identifier, must not be null
+ */
+ public ModelTurnId {
+ Objects.requireNonNull(value, "value must not be null");
+ }
+
+ /**
+ * Creates a model turn identity from its textual form.
+ * @param value the opaque model turn identifier, must not be null
+ * @return the model turn identity
+ */
+ public static ModelTurnId of(String value) {
+ return new ModelTurnId(value);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/StepId.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/StepId.java
new file mode 100644
index 000000000..eebb9bca0
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/StepId.java
@@ -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 step inside an invocation.
+ *
+ *
+ * A step is a unit of work that may contain model turns and tool calls. A step is
+ * stable across retries of the same work; the attempts below it are not.
+ *
+ * @param value the opaque step identifier
+ */
+public record StepId(String value) {
+
+ /**
+ * Creates a step identity.
+ * @param value the opaque step identifier, must not be null
+ */
+ public StepId {
+ Objects.requireNonNull(value, "value must not be null");
+ }
+
+ /**
+ * Creates a step identity from its textual form.
+ * @param value the opaque step identifier, must not be null
+ * @return the step identity
+ */
+ public static StepId of(String value) {
+ return new StepId(value);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallExecutor.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallExecutor.java
new file mode 100644
index 000000000..46ced3cd2
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallExecutor.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Executes a tool call in the neutral lifecycle contract.
+ *
+ *
+ * The executor is the continuation an around interceptor wraps. It performs the actual
+ * tool execution and returns its outcome.
+ */
+@FunctionalInterface
+public interface ToolCallExecutor {
+
+ /**
+ * Executes the tool call.
+ * @param invocation the tool call to execute, never null
+ * @return the outcome of the call, never null
+ */
+ ToolCallOutcome execute(ToolCallInvocation invocation);
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallId.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallId.java
new file mode 100644
index 000000000..3826bf884
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallId.java
@@ -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 tool call inside an invocation.
+ *
+ *
+ * A tool call is one request to a tool binding and its result. Retries of the same
+ * tool call keep the same identity and differ in their attempt identity.
+ *
+ * @param value the opaque tool call identifier
+ */
+public record ToolCallId(String value) {
+
+ /**
+ * Creates a tool call identity.
+ * @param value the opaque tool call identifier, must not be null
+ */
+ public ToolCallId {
+ Objects.requireNonNull(value, "value must not be null");
+ }
+
+ /**
+ * Creates a tool call identity from its textual form.
+ * @param value the opaque tool call identifier, must not be null
+ * @return the tool call identity
+ */
+ public static ToolCallId of(String value) {
+ return new ToolCallId(value);
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallInvocation.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallInvocation.java
new file mode 100644
index 000000000..c8e2db8f8
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallInvocation.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+/**
+ * A tool call in the neutral lifecycle contract.
+ *
+ *
+ * The invocation is immutable. A restricted transformer may replace the arguments of
+ * a tool call, but neither the identity, the tool call identity nor the tool name may
+ * change: renaming a tool inside a transformation would silently redirect the call to
+ * a different binding, which is an admission decision, not a transformation.
+ *
+ *
+ * The type deliberately carries no free-form context map. Extensions that need
+ * additional data read it from the run state or the trusted execution context instead
+ * of an untyped side channel.
+ *
+ * @param identity the identity of the work this call belongs to, never null
+ * @param toolCall the tool call identity, never null
+ * @param toolName the name of the tool binding, never null
+ * @param arguments the serialized tool arguments, never null
+ */
+public record ToolCallInvocation(LifecycleIdentity identity, ToolCallId toolCall, String toolName, String arguments) {
+
+ /**
+ * Creates a tool call invocation.
+ * @param identity the identity of the work this call belongs to, must not be null
+ * @param toolCall the tool call identity, must not be null
+ * @param toolName the name of the tool binding, must not be null
+ * @param arguments the serialized tool arguments, must not be null
+ */
+ public ToolCallInvocation {
+ Objects.requireNonNull(identity, "identity must not be null");
+ Objects.requireNonNull(toolCall, "toolCall must not be null");
+ Objects.requireNonNull(toolName, "toolName must not be null");
+ Objects.requireNonNull(arguments, "arguments must not be null");
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallOutcome.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallOutcome.java
new file mode 100644
index 000000000..2b8c2fd59
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/ToolCallOutcome.java
@@ -0,0 +1,74 @@
+/*
+ * 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 result of a tool call in the neutral lifecycle contract.
+ *
+ *
+ * A tool call either succeeds with a result or fails with an error description.
+ * Exactly one of {@code result} and {@code error} is present.
+ *
+ * @param toolCall the tool call identity, never null
+ * @param result the tool result, nullable
+ * @param error the failure description, nullable
+ */
+public record ToolCallOutcome(ToolCallId toolCall, String result, String error) {
+
+ /**
+ * Creates a tool call outcome.
+ * @param toolCall the tool call identity, must not be null
+ * @param result the tool result, nullable
+ * @param error the failure description, nullable
+ */
+ public ToolCallOutcome {
+ Objects.requireNonNull(toolCall, "toolCall must not be null");
+ if ((result == null) == (error == null)) {
+ throw new IllegalArgumentException("exactly one of result and error must be present");
+ }
+ }
+
+ /**
+ * Creates a successful outcome.
+ * @param toolCall the tool call identity, must not be null
+ * @param result the tool result, must not be null
+ * @return the successful outcome
+ */
+ public static ToolCallOutcome success(ToolCallId toolCall, String result) {
+ return new ToolCallOutcome(toolCall, result, null);
+ }
+
+ /**
+ * Creates a failed outcome.
+ * @param toolCall the tool call identity, must not be null
+ * @param error the failure description, must not be null
+ * @return the failed outcome
+ */
+ public static ToolCallOutcome failure(ToolCallId toolCall, String error) {
+ return new ToolCallOutcome(toolCall, null, error);
+ }
+
+ /**
+ * Whether the call succeeded.
+ * @return true when a result is present
+ */
+ public boolean isSuccess() {
+ return result != null;
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/test/java/io/github/agentic/ai/agent/lifecycle/LifecycleContractTest.java b/argi-agent/argi-agent-api/src/test/java/io/github/agentic/ai/agent/lifecycle/LifecycleContractTest.java
new file mode 100644
index 000000000..4d34db18d
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/test/java/io/github/agentic/ai/agent/lifecycle/LifecycleContractTest.java
@@ -0,0 +1,704 @@
+/*
+ * 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 io.github.agentic.ai.agent.interceptor.ModelRequest;
+import io.github.agentic.ai.runtime.execution.ExecutionBudget;
+import io.github.agentic.ai.runtime.execution.DelegationScope;
+import io.github.agentic.ai.runtime.execution.PrincipalReference;
+import io.github.agentic.ai.runtime.execution.TenantReference;
+import io.github.agentic.ai.runtime.execution.TrustedExecutionContext;
+import io.github.agentic.ai.agent.interceptor.ModelResponse;
+import io.github.agentic.ai.api.chat.ChatMessage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Contract tests for the neutral lifecycle extension points.
+ *
+ *
+ * The tests drive the four extension kinds directly, without a model, a tool binding
+ * or an engine. The chain executor and the engine wiring are separate issues; what
+ * this test pins down is the semantics of the contract itself: observation,
+ * restricted transformation, rejection, wrapping and failure handling.
+ */
+class LifecycleContractTest {
+
+ private static final InvocationId INVOCATION = InvocationId.of("inv-1");
+
+ private static final ModelTurnId MODEL_TURN = ModelTurnId.of("turn-1");
+
+ private static final ToolCallId TOOL_CALL = ToolCallId.of("call-1");
+
+ private static final List
+ * The harness is deliberately part of the test: the chain executor and the engine
+ * wiring are separate issues, and this test pins down the contract semantics only.
+ */
+ private static final class StageRunner {
+
+ private final List
+ *
+ *
+ * @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
+ }
+
+}
diff --git a/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/InvocationId.java b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/InvocationId.java
new file mode 100644
index 000000000..56023cd1a
--- /dev/null
+++ b/argi-agent/argi-agent-api/src/main/java/io/github/agentic/ai/agent/lifecycle/InvocationId.java
@@ -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 agent invocation.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *