Plugin actions
This page describes the plugin action model, including lifecycle-triggered actions, as a completed runtime feature.
The core idea is that plugins expose one action concept, and actions may be:
- manually triggered by a user or UI
- triggered automatically by a lifecycle event
The distinction between application plugins and other plugin types is not the action naming. The distinction is where actions run, what they are allowed to return, and whether they are limited to a single session mutation scope.
Runtime references for the current codebase:
core/python/agent_app/app_plugins.pycore/python/agent_app/application_future.pycore/python/agent_core/types.pycore/python/agent_core/core.pyapplication/python/agent_terminal_app/server.py
Overview
After the update, actions will follow these rules:
- Application plugins continue to define actions with
get_actions(...)and execute them withexecute_action(...). - Provider extensions use the same action concept and the same method names for session-scoped actions.
- Lifecycle-triggered work is modeled as an ordinary action with a
triggerfield, rather than a second hook naming scheme. AgentApplicationdecides when lifecycle-triggered actions run.AgentCoreexecutes lifecycle-triggered actions for core-side plugins and returns a modifiedSession.
Generic display payloads
Successful actions may return a generic user-facing display payload in
result.display.
This is the preferred way for plugins to tell frontends to show instructions, status summaries, or rich follow-up text without introducing plugin-specific result keys.
Recommended shape:
{
"display": {
"format": "markdown",
"title": "ChatGPT Login",
"body": "Open [this link](https://example.test) and enter code `ABCD-EFGH`.",
"variant": "info",
"presentation": "inline",
"dismissible": true,
"display_id": "chatgpt-auth-login",
"actions": [
{
"kind": "open_url",
"id": "open-login-link",
"label": "Open link",
"url": "https://example.test"
},
{
"kind": "copy_text",
"id": "copy-login-code",
"label": "Copy code",
"text": "ABCD-EFGH"
},
{
"kind": "run_action",
"id": "check-login-status",
"label": "Check status",
"plugin": "chatgpt_auth",
"action_id": "check_status",
"preserve_display": true
}
]
}
}
Fields:
format:"markdown"or"text"body: required display contenttitle: optional headingvariant: optional tone hint such asinfo,success,warning, orerrorpresentation: optional frontend hint such asmodal,inline, orbannerdismissible: optional hint controlling whether the user can dismiss the rendered displaydisplay_id: optional stable id so frontends can update or replace an existing inline/banner display instead of always appending a new oneactions: optional list of generic follow-up actions
Guidance:
- keep
displayuser-oriented and safe to render directly - keep machine-readable fields alongside it when frontend logic or tests still need structured state
- do not overload top-level
messagekeys for successful rich content whendisplayis more appropriate - keep follow-up actions intentionally narrow and generic; current supported
shapes are
open_url,copy_text, andrun_action - treat
presentationas a frontend hint, not a guarantee: current frontends ignoredisplayforsession_list_actionsurfaces and the desktop app currently coerces session-action displays toinline modalremains part of the protocol, but richer cross-platform popup support should be added later only when there is a concrete use case and a UI approach that works on both mobile and desktop
Dynamic lifecycle names
Lifecycle names are dynamic strings.
That means:
- future application implementations may call
AgentApplicationwith arbitrary lifecycle names - future plugins may declare arbitrary lifecycle names in
trigger - the contract should not require lifecycle names to be chosen from a fixed enum baked into the API
The system still has a standard built-in lifecycle baseline for normal app flows, but that baseline is not the full allowed set.
Triggered actions
An action definition may include a trigger field.
Examples:
"trigger": "session_list""trigger": "session_create""trigger": "request_prepare""trigger": "session_fork""trigger": ["session_create", "request_prepare"]"trigger": "my_future_custom_lifecycle"
The meaning of common triggers is:
session_list: the application/server is building session-list summary datasession_create: a new session has just been created and may need initial session-owned settingssession_save_prepare: a session is about to be persisted and may need final metadata adjustmentsrequest_prepare: an existing session is about to be used for a request and may need repair or fallback initializationrequest_complete: a request completed successfully and post-request session metadata may need to be updatedresponse_finalize: core-owned post-finalization stage where in-flight final messages and retained native history may still be adjusted before the final payload is emittedrequest_error: a request failed and failure-related session state may need to be recordedsession_fork: a new forked session has been created from an existing session and may need fork-aware adjustmentsagent_switch_prepare: a session is about to switch from one agent to another and may need pre-switch adjustmentsagent_switch_complete: a session has completed an agent switch and may need post-switch adjustmentssession_delete_prepare: a session is about to be deleted and plugins may need to record or clean up related state
Actions without a trigger are manual actions.
Standard built-in lifecycle set
Even though lifecycle names are dynamic, AgentApplication provides a
standard built-in set for its mutating flows.
The standard built-in lifecycles are:
session_createsession_save_preparerequest_preparerequest_completerequest_errorsession_forkagent_switch_prepareagent_switch_completesession_delete_prepare
Lifecycle meanings:
session_create: run after a brand-new session is created and before first persistencesession_save_prepare: run immediately before a session is saved to the session store; intended for final metadata normalization or session-owned bookkeeping that must happen before persistencerequest_prepare: run after effective config resolution and before the tool loop/provider request beginsrequest_complete: run after a successful request has produced its final session and before the application completes post-request persistencerequest_error: run after a request fails and before the application exits the request flow; intended for recording failure metadata or clearing partially initialized valuessession_fork: run on the new forked session after the fork is created; fork-specific context may include the serialized source sessionagent_switch_prepare: run when a session is about to switch from one agent to anotheragent_switch_complete: run after the session has been updated to its new agent assignmentsession_delete_prepare: run before a session is deleted; intended for any last-chance session-scoped cleanup or bookkeeping
The built-in lifecycle set is the standard vocabulary used by
AgentApplication. It intentionally excludes load/read paths, because built-in
lifecycle actions are allowed to be mutating and may be long-running. Callers
may still use additional lifecycle names when they need application-specific
behavior.
Lifecycle timing and ownership
The standard lifecycles are triggered at these points:
session_create: owned byAgentApplicationsession creation flows such ascreate_session_ephemeral(...)and any persisted create flow built on top of itsession_save_prepare: owned byAgentApplication.save_session(...)request_prepare: owned byAgentApplication.send_request(...), after effective config resolution and before the tool loop/provider request startsrequest_complete: owned byAgentApplication.send_request(...), after the request finishes successfully and a final session existsresponse_finalize: owned byAgentCorerequest execution, after provider / feature native finalization and native-to-core conversion, but before final messages are emitted or appended to the sessionrequest_error: owned byAgentApplication.send_request(...), when request execution raises or returns a terminal error pathsession_fork: owned by session-fork flows in the application layer and runs on the new forked sessionagent_switch_prepare: owned by agent-switch flows before the new agent id is written to the sessionagent_switch_complete: owned by agent-switch flows after the new agent id has been writtensession_delete_prepare: owned by application-layer delete-session flows
AgentCore does not decide when any of these lifecycles happen. It only
executes matching core-side actions for the lifecycle name that
AgentApplication passes in.
Exception:
response_finalizeis a core-owned lifecycle executed directly byAgentCoreduring request finalization so core-side plugins can still mutate the current turn'sfinal_messagesbefore they are returned.
Dispatcher ownership
Lifecycle dispatch belongs to AgentApplication.
Reasons:
AgentApplicationowns session creation and request entrypoints.AgentApplicationowns session persistence, locks, checkpoints, and event publication.AgentApplicationcan coordinate both application plugins and core-side plugins from one place.
AgentApplication also provides a generic lifecycle-dispatch entry point that
accepts:
- a lifecycle name
- the current session
- the effective config
- optional extra context
This is what allows future application implementations to run arbitrary lifecycle names beyond the built-in set.
The intended flow is:
AgentApplicationdecides that a lifecycle trigger should run.AgentApplicationexecutes matching application-plugin actions.AgentApplicationcalls a high-levelAgentCorehelper for matching provider-extension or feature actions.AgentApplicationpersists any changed session through the normal application-layer save/checkpoint path.
For the standard built-in set, the generic flow is:
- determine the lifecycle name and effective config
- build serializable execution context
- run matching application-plugin actions
- run matching core-side actions through
AgentCore - merge resulting session mutations
- continue the owning application operation such as save, request, fork, or agent switch
For response_finalize, the flow is slightly different:
AgentCorefinishes provider / feature nativefinalize(...)AgentCoreconverts the current turn's native finals back into corefinal_messagesAgentCoreruns matching core-side actions withtrigger: "response_finalize"- actions may return replacement
final_messages, replacementnative_messages, andsession_metadata AgentCoreemits / returns the updatedfinal_messagesand stores the updated native history
Execution context
Lifecycle-triggered action execution may include extra context in addition to normal action params.
This context exists so callers can provide lifecycle-specific information such as the original session used to create a fork.
Requirements:
- extra context must be serializable
- plugin code must treat extra context as language-neutral data
- multi-language plugin implementations must not depend on Python object identity or Python-only types in this context channel
Recommended examples:
{"lifecycle": "request_prepare"}{"lifecycle": "session_fork", "original_session": {...}}{"lifecycle": "agent_switch_prepare", "previous_agent_id": "a", "next_agent_id": "b"}{"lifecycle": "request_error", "error": {"type": "RuntimeError", "message": "tool loop failed"}}{"lifecycle": "session_delete_prepare", "delete_reason": "user_request"}
When a fork lifecycle is triggered, the original session is passed in a
serialized form such as the result of Session.to_dict(), not as a live
Python Session object.
Reserved context keys
The runtime may populate these commonly used keys in context:
lifecycle: the lifecycle name being executedtrigger_source: string describing who initiated execution, such asapplication,server, ormanualoriginal_session: serialized source session for fork-style flowsprevious_agent_id: source agent id for agent-switch flowsnext_agent_id: destination agent id for agent-switch flowserror: serialized error payload for failure lifecycles
Plugins may also use additional serializable context keys supplied by the caller.
Reserved context keys are owned by the core/application builders. Caller context is additive only. If caller-provided context reuses a reserved key, the builder keeps the owned value and emits a warning instead of silently overriding it.
Layered context for feature/extension plugins
Feature plugins and provider extensions receive layered context that provides different capabilities depending on where the action is triggered. This allows plugins to access enhanced capabilities when available while gracefully degrading when running in core-only mode.
Layer 1: Core-level context (always present)
When called from AgentCore.execute_session_action() or AgentCore.execute_lifecycle_actions():
context = {
"core": self, # AgentCore instance
"config": config, # Current resolved config
"trigger_source": "core", # Where action was triggered
"session": session.to_dict(), # Serialized session
"lifecycle": trigger, # Lifecycle trigger name (if applicable)
}
| Key | Type | Description |
|---|---|---|
core |
AgentCore |
Core instance for session/message operations |
config |
dict |
Current resolved request configuration |
trigger_source |
str |
Where action was triggered ("core", "application") |
session |
dict |
Serialized session (session.to_dict()) |
lifecycle |
str |
Lifecycle trigger name (for lifecycle-triggered actions) |
request_context |
dict |
Request-initialized plugin context for action-style flows |
request_runtime |
dict |
Request runtime helpers (provider, features) for action-style flows |
Layer 2: Application-level context (when called from AgentApplication)
When called from AgentApplication.execute_session_action() or AgentApplication.run_session_lifecycle():
context = {
# Layer 1 (from core)
"core": core,
"config": effective_config,
"trigger_source": "application",
"session": session.to_dict(),
"lifecycle": lifecycle, # if applicable
# Layer 2 (from application)
"app": self, # AgentApplication instance
"application": self, # Alias for "app"
"base_config": self._config, # Full base config (all agents)
"session_asset_store": self._session_asset_store, # Optional
}
| Key | Type | Description |
|---|---|---|
app / application |
AgentApplication |
Application instance for enhanced capabilities |
base_config |
dict |
Full application configuration (all agents) |
session_asset_store |
SessionAssetStore |
File attachment store (optional) |
When application and core both contribute context, ownership is explicit:
application contributes application-owned keys such as app,
application, base_config, and session_asset_store, while the core
contributes core-owned keys such as core, config, session,
trigger_source, request_context, and request_runtime.
Layer 3: Terminal/Server-level context (implementation-specific)
Terminal or HTTP server implementations may add additional context keys:
# TerminalApplication may add:
context["terminal"] = self
# HTTP server may add:
context["server"] = self
context["request_id"] = request_id
These keys are implementation-specific and should be documented by the respective applications.
Checking for capabilities
Feature and extension plugins should check for capabilities before using them:
def execute_action(
self,
action_id: str,
session: Session,
native_messages: List[Dict[str, Any]],
params: Dict[str, Any],
context: Optional[Dict[str, Any]],
state: Dict[str, Any],
) -> Dict[str, Any]:
# Core is always available
core = context["core"]
config = context["config"]
# Application is optional (check before using)
app = context.get("app")
base_config = context.get("base_config", config)
if app is not None:
# Application-level capabilities available
# Can switch agents, use full request flow
pass
else:
# Core-only mode
# Use core.send_request() for same-agent requests
pass
Note on config resolution: The config key is the resolved request configuration for the current agent. When called from AgentApplication, base_config contains the full flattened agent configuration (from build_agent_config()). For agent-level settings like compaction, always use base_config when available, falling back to config for core-only calls.
Context flow diagram
TerminalApplication.execute_action()
↓ adds: {"terminal": self, ...}
AgentApplication.run_session_lifecycle()
↓ adds: {"app": self, "base_config": ...}
AgentCore.execute_lifecycle_actions()
↓ adds: {"core": self, "config": config}
FeaturePlugin.execute_action(session, native_messages, params, context, state)
↓ Plugin checks: app = context.get("app")
core = context["core"]
Application plugin contract
Application plugins keep the current action method names and semantics.
class ApplicationPlugin(Protocol):
name: str
version: str
def init(self, app_config: dict[str, Any]) -> dict[str, Any]:
...
def get_actions(self, state: dict[str, Any]) -> list[dict[str, Any]]:
...
def execute_action(
self,
app: "AgentApplication",
action_id: str,
params: dict[str, Any],
context: dict[str, Any] | None,
state: dict[str, Any],
) -> dict[str, Any]:
...
Application-plugin action definitions may be manual or lifecycle-triggered.
Example:
{
"id": "ensure_session_defaults",
"label": "Ensure session defaults",
"inputs": {
"session_id": {"type": "string", "required": True},
},
"trigger": ["session_create", "request_prepare"],
}
params remains the plugin-defined action input surface.
context is execution context supplied by the caller, especially for
lifecycle-triggered execution. It may contain arbitrary serializable keys.
Application-plugin action results remain application-level. They may include:
mutationsfor created, updated, or deleted sessionsui_effectssuch as reload requests or navigation hints- additional app-facing payloads used by HTTP, terminal, or mobile flows
When application actions are lifecycle-triggered, they still use the same contract. The difference is only how they were selected for execution.
This is different from core-side plugin actions, which stay single-session oriented.
Provider extension contract
Provider extensions use the same action naming pattern for this concept.
class ProviderExtensionPlugin(BasePlugin, Protocol):
name: str
version: str
def init(self, config: dict[str, Any], state: dict[str, Any]) -> dict[str, Any]:
...
def get_actions(self, state: dict[str, Any]) -> list[dict[str, Any]]:
...
def execute_action(
self,
action_id: str,
session: Session,
native_messages: list[dict[str, Any]],
params: dict[str, Any],
context: dict[str, Any] | None,
state: dict[str, Any],
) -> dict[str, Any]:
...
The important difference is the result contract.
Provider-extension actions stay single-session oriented. They do not return application-level UI effects or navigation instructions. Instead, they follow the existing core-side mutation style:
- return replacement
native_messageswhen the visible message list should be rebuilt from provider-native history - return
session_metadatawhen session metadata should be patched
Because session_metadata can contain "overrides", provider-extension
actions can initialize or repair session-owned settings without becoming
application plugins.
context follows the same rule here: it is caller-provided execution metadata
and must be serializable.
Feature plugin contract
Features that participate in triggered session actions follow the same
conceptual pattern as provider extensions: get_actions(...) plus
execute_action(...), with single-session mutation results rather than
application-level UI effects.
Core-side session mutation model
For provider extensions and similar non-application plugins, the core-side action result is intentionally narrow.
Expected mutation surfaces:
native_messagessession_metadata
Expected outcomes:
- visible messages may change when the core rebuilds the transcript from
returned
native_messages - session-owned settings may change when
session_metadatapatchessession.metadata["overrides"]
The goal is to keep core-side actions limited to one session. They do not own application concerns such as reload-all, switch-session, or multi-session UI coordination.
Lifecycle action ordering
Actions are executed in the same stable plugin/action ordering used elsewhere in the runtime.
Practical rules:
- application-plugin lifecycle actions run in application-plugin registration order
- core-side lifecycle actions run in the resolved provider/plugin order for the effective config
- for a single plugin, actions run in the order returned by
get_actions(...) - later actions observe session changes produced by earlier actions in the same lifecycle run
Plugins should therefore make lifecycle actions idempotent when possible.
Lifecycle examples
Create-time initialization
A provider extension can ensure a session-owned setting exists for a new session:
{
"id": "ensure_prompt_cache_key",
"label": "Ensure prompt cache key",
"inputs": {},
"trigger": "session_create",
}
Its execute_action(...) may return:
{
"native_messages": native_messages,
"session_metadata": {
"overrides": {
"prompt_cache_key": "generated-key"
}
},
}
Request-time repair
The same action can also be triggered for older sessions that predate the setting:
{
"id": "ensure_prompt_cache_key",
"label": "Ensure prompt cache key",
"inputs": {},
"trigger": ["session_create", "request_prepare"],
}
AgentApplication is responsible for deciding that request_prepare
run before the request/tool loop begins and for persisting any resulting
session change.
Save-time normalization
Plugins can normalize session metadata before persistence:
{
"id": "normalize_before_save",
"label": "Normalize before save",
"inputs": {},
"trigger": "session_save_prepare",
}
Request completion and error handling
Plugins can react after request completion or failure:
{
"id": "record_request_success",
"label": "Record request success",
"inputs": {},
"trigger": "request_complete",
}
{
"id": "record_request_failure",
"label": "Record request failure",
"inputs": {},
"trigger": "request_error",
}
Fork-aware initialization
A plugin can also react to a newly created fork:
{
"id": "adjust_fork_settings",
"label": "Adjust fork settings",
"inputs": {},
"trigger": "session_fork",
}
The caller may provide extra context like:
{
"lifecycle": "session_fork",
"original_session": {
"session_id": "session-123",
"messages": [],
"metadata": {"agent_id": "default"},
},
}
That lets the action make fork-aware decisions without relying on Python-only objects.
Agent switch handling
Plugins can react before and after an agent switch:
{
"id": "prepare_for_agent_switch",
"label": "Prepare for agent switch",
"inputs": {},
"trigger": "agent_switch_prepare",
}
{
"id": "finalize_agent_switch",
"label": "Finalize agent switch",
"inputs": {},
"trigger": "agent_switch_complete",
}
Context commonly includes:
{
"lifecycle": "agent_switch_prepare",
"previous_agent_id": "default",
"next_agent_id": "research",
}
Delete-time cleanup
Plugins can run one final action before deletion:
{
"id": "cleanup_before_delete",
"label": "Cleanup before delete",
"inputs": {},
"trigger": "session_delete_prepare",
}
Design guidance
Use an application plugin when the action needs application-level powers such as:
- creating or deleting sessions
- coordinating multiple sessions
- returning UI navigation or reload instructions
- integrating with server-only or frontend-only flows
Use a provider extension or similar core-side plugin when the action is owned by one session and one provider/config context, especially when it needs to:
- mutate provider-native history
- patch session metadata owned by that provider feature
- ensure or repair session-owned settings such as values stored under
session.metadata["overrides"]
Relation to existing session_list actions
The current code already has one server-interpreted triggered-action pattern:
application-plugin actions with trigger: "session_list".
The runtime generalizes that idea so lifecycle triggers are no longer a server-only special case and can also be used by non-application plugins through shared application/core orchestration.
Core-side lifecycle vocabulary
AgentCore supports execution for any lifecycle name that
AgentApplication passes to it.
The standard vocabulary it receives includes:
session_createsession_save_preparerequest_preparerequest_completerequest_errorsession_forkagent_switch_prepareagent_switch_completesession_delete_prepare
AgentCore does not decide when these lifecycles happen. It only executes
matching core-side actions for the given lifecycle name, session, config, and
serializable execution context.