diff --git a/docs.json b/docs.json
index 6a19e6760..38909d7b7 100644
--- a/docs.json
+++ b/docs.json
@@ -684,6 +684,7 @@
"group": "Component Development",
"pages": [
"en/plugin/dev/components/add",
+ "en/plugin/dev/components/runner",
"en/plugin/dev/components/event-listener",
"en/plugin/dev/components/command",
"en/plugin/dev/components/tool",
@@ -706,7 +707,9 @@
{
"group": "Plugin SDK API",
"pages": [
+ "en/plugin/dev/apis/agent-run",
"en/plugin/dev/apis/common",
+ "en/plugin/dev/apis/platform",
"en/plugin/dev/apis/pipeline-events",
"en/plugin/dev/apis/messages",
"en/plugin/dev/apis/tech-details"
@@ -988,6 +991,7 @@
"group": "组件开发",
"pages": [
"zh/plugin/dev/components/add",
+ "zh/plugin/dev/components/runner",
"zh/plugin/dev/components/event-listener",
"zh/plugin/dev/components/command",
"zh/plugin/dev/components/tool",
@@ -1010,7 +1014,9 @@
{
"group": "插件 SDK API",
"pages": [
+ "zh/plugin/dev/apis/agent-run",
"zh/plugin/dev/apis/common",
+ "zh/plugin/dev/apis/platform",
"zh/plugin/dev/apis/pipeline-events",
"zh/plugin/dev/apis/messages",
"zh/plugin/dev/apis/tech-details"
@@ -1287,6 +1293,7 @@
"group": "コンポーネント開発",
"pages": [
"ja/plugin/dev/components/add",
+ "ja/plugin/dev/components/runner",
"ja/plugin/dev/components/event-listener",
"ja/plugin/dev/components/command",
"ja/plugin/dev/components/tool",
@@ -1309,7 +1316,9 @@
{
"group": "プラグイン SDK API",
"pages": [
+ "ja/plugin/dev/apis/agent-run",
"ja/plugin/dev/apis/common",
+ "ja/plugin/dev/apis/platform",
"ja/plugin/dev/apis/pipeline-events",
"ja/plugin/dev/apis/messages",
"ja/plugin/dev/apis/tech-details"
diff --git a/en/plugin/dev/apis/agent-run.mdx b/en/plugin/dev/apis/agent-run.mdx
new file mode 100644
index 000000000..58bcf069f
--- /dev/null
+++ b/en/plugin/dev/apis/agent-run.mdx
@@ -0,0 +1,261 @@
+---
+title: "Context APIs"
+---
+
+A context contains data for one component invocation. Each component receives a different context, so start with the entry point for your component.
+
+| Component | Handler entry point | Context type |
+| --- | --- | --- |
+| EventListener | `handler(event_context)` | `EventContext` |
+| Command | `subcommand(context)` | `ExecuteContext` |
+| Tool | `call(params, session, query_id)` | No separate context object |
+| Runner | `run(ctx)` or `handler(ctx)` | `RunnerContext` |
+| KnowledgeEngine | `ingest(context)`, `retrieve(context)` | `IngestionContext`, `RetrievalContext` |
+| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` |
+| Parser | `parse(context)` | `ParseContext` |
+| Page | `handle_api(request)` | `PageRequest` |
+
+This page covers the data and methods available through these contexts. Use `self.plugin` for models, knowledge bases, storage, and other LangBot capabilities; see [LangBot API](/en/plugin/dev/apis/common). For platform operations, see [Platform API](/en/plugin/dev/apis/platform).
+
+`EventContext`, `ExecuteContext`, and `Session` also carry `instance_uuid`, `workspace_uuid`, and `placement_generation`. These fields identify the current instance and workspace for execution; plugins must not treat them as authorization data.
+
+## EventListener
+
+EventListener handles Pipeline events. Its event handler receives an `EventContext`:
+
+```python
+event = event_context.event
+sender_id = event.sender_id
+```
+
+### Context data
+
+| Property | Type | Description |
+| --- | --- | --- |
+| `event` | `BaseEventModel` | Current event object; its actual type matches the registered event type |
+| `event_name` | `str` | Current event class name |
+| `query_id` | `int` | Current Pipeline request ID |
+| `query_uuid` | `str \| None` | Stable identifier for the current Pipeline request |
+| `eid` | `int` | Temporary event number inside Plugin Runtime; do not persist it |
+| `is_prevent_default` | `bool` | Whether default handling has been stopped; change it with `prevent_default()` |
+| `is_prevent_postorder` | `bool` | Whether later plugins have been stopped; change it with `prevent_postorder()` |
+
+See [Pipeline Events](/en/plugin/dev/apis/pipeline-events) for event fields and all supported event types.
+
+### Context methods
+
+| Method | Return value | Description |
+| --- | --- | --- |
+| `await event_context.reply(message_chain, quote_origin=False)` | - | Reply in the conversation for the current request |
+| `await event_context.get_bot_uuid()` | `str` | Get the source bot UUID |
+| `await event_context.set_query_var(key, value)` | - | Set a variable on the current request |
+| `await event_context.get_query_var(key)` | `Any` | Get one request variable |
+| `await event_context.get_query_vars()` | `dict[str, Any]` | Get all request variables |
+| `await event_context.create_new_conversation()` | `dict[str, Any]` | Clear the current Pipeline conversation so a later request starts a new one |
+| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | List knowledge bases bound to the current Pipeline Local Agent |
+| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | Search a knowledge base bound to the current Pipeline |
+| `event_context.prevent_default()` | - | Stop the default handling of the current event |
+| `event_context.prevent_postorder()` | - | Stop later plugins from handling the current event |
+
+`reply()` accepts a [MessageChain](/en/plugin/dev/apis/messages). Request variables, replies, and Pipeline knowledge-base methods require an associated Pipeline request. The `kb_id` passed to `retrieve_knowledge()` must come from `list_pipeline_knowledge_bases()`.
+
+`prevent_default()` only affects Pipeline events whose default flow can be stopped. See [Pipeline Events](/en/plugin/dev/apis/pipeline-events#prevent-default-behavior).
+
+## Command
+
+A Command subcommand handler receives an `ExecuteContext`:
+
+```python
+query = " ".join(context.crt_params)
+session = context.session
+```
+
+### Context data
+
+| Property | Type | Description |
+| --- | --- | --- |
+| `session` | `Session` | Session that contains the current message |
+| `command_text` | `str` | Complete command text without the command prefix |
+| `full_command_text` | `str` | Complete command text including the prefix |
+| `command` | `str` | Root command name |
+| `crt_command` | `str` | Subcommand currently being executed |
+| `params` | `list[str]` | All arguments after the root command |
+| `crt_params` | `list[str]` | Arguments not yet consumed by the current subcommand |
+| `privilege` | `int` | Command privilege level of the current user |
+| `query_id` | `int` | Current Pipeline request ID |
+| `query_uuid` | `str \| None` | Stable identifier for the current Pipeline request |
+
+The SDK calls `context.shift()` before entering the current subcommand. Components do not need to call it directly.
+
+### Context methods
+
+| Method | Return value | Description |
+| --- | --- | --- |
+| `await context.reply(message_chain, quote_origin=False)` | - | Reply in the conversation for the current request |
+| `await context.get_bot_uuid()` | `str` | Get the source bot UUID |
+| `await context.set_query_var(key, value)` | - | Set a variable on the current request |
+| `await context.get_query_var(key)` | `Any` | Get one request variable |
+| `await context.get_query_vars()` | `dict[str, Any]` | Get all request variables |
+| `await context.create_new_conversation()` | `dict[str, Any]` | Clear the current Pipeline conversation so a later request starts a new one |
+| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | List knowledge bases bound to the current Pipeline Local Agent |
+| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | Search a knowledge base bound to the current Pipeline |
+
+These request methods behave the same as their EventListener equivalents. See [Command](/en/plugin/dev/components/command) for registration and return values.
+
+## Tool
+
+Tool has no separate context object. Its `call()` parameters carry the invocation data:
+
+```python
+conversation_type = session.launcher_type.value
+conversation_id = session.launcher_id
+sender_id = session.sender_id
+```
+
+| Parameter | Description |
+| --- | --- |
+| `params` | Arguments generated by the model or caller from the tool JSON Schema |
+| `session` | Current session data |
+| `query_id` | Current Pipeline request ID; pass it unchanged to LangBot APIs that require request context |
+
+Common `Session` properties are:
+
+| Property | Description |
+| --- | --- |
+| `launcher_type` | Conversation type: `person` or `group` |
+| `launcher_id` | Private-chat user or group ID |
+| `sender_id` | Current message sender ID |
+| `bot_uuid` | Current bot UUID |
+| `using_conversation` | Current Pipeline conversation |
+| `conversations` | Pipeline conversations stored in this Session |
+| `use_prompt_name` | Active prompt name |
+| `create_time`, `update_time` | Session creation and update times |
+
+`session` is a data object and has no context methods such as `reply()`. A tool return value goes back to its caller; it is not sent to the chat platform automatically. Use `self.plugin` for LangBot and platform capabilities. If an API requires `session` and `query_id`, pass the values received by the current call.
+
+## Runner
+
+Both custom `run(ctx)` implementations and Runner event handlers receive a `RunnerContext`. It remains valid only while the current invocation is running.
+
+```python
+text = ctx.input.to_text()
+event_type = ctx.event.event_type
+```
+
+### Context data
+
+| Property | Type | Description |
+| --- | --- | --- |
+| `run_id` | `str` | Current run ID |
+| `trigger` | `AgentTrigger` | Trigger type, source, and time |
+| `event` | `AgentEventContext` | Standard event envelope with the event ID, type, source, time, and raw data |
+| `platform_event` | Platform event type | Platform event parsed into its specific event type |
+| `conversation` | `ConversationContext \| None` | Current conversation, thread, bot, and workspace identifiers |
+| `actor` | `ActorContext \| None` | Actor that caused the event |
+| `subject` | `SubjectContext \| None` | Message, group, or other object affected by the event |
+| `input` | `AgentInput` | Current text, structured content, and attachments |
+| `delivery` | `DeliveryContext` | Output target and streaming, editing, and reaction capabilities |
+| `resources` | `AgentResources` | Models, tools, knowledge bases, skills, and storage authorized for this run |
+| `context` | `ContextAccess` | Conversation cursors, inline policy, and available context APIs |
+| `state` | `AgentRunState` | Scope-specific state snapshot loaded at the start of the run |
+| `runtime` | `AgentRuntimeContext` | LangBot version, trace ID, and deadline |
+| `config` | `dict[str, Any]` | Current Agent or plugin-processor configuration |
+| `adapter` | `AdapterContext \| None` | Additional entry-adapter data |
+| `variables` | `dict[str, Any]` | Public request variables for Runner templates |
+| `metadata` | `dict[str, Any]` | Other metadata supplied by the Host |
+
+Prefer the stable properties in this table. `event.data` and `adapter.extra` carry additional data that has not been promoted to stable fields.
+
+### Replies and logs
+
+| Method | Return value | Description |
+| --- | --- | --- |
+| `await ctx.get_bot_uuid()` | `str` | Get the source bot UUID; raises an error when no bot is associated |
+| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | Reply to the current event with a string or `MessageChain` |
+| `ctx.reply_stream()` | Async context manager | Stream full-text updates to one reply |
+| `await ctx.log(text, level="info")` | - | Write to this run's log; levels are `debug`, `info`, `warning`, and `error` |
+
+Each `reply_stream().update(text)` call supplies the complete current text. If the platform does not support streaming, LangBot sends one complete message when the stream finishes. Debug mode only simulates delivery.
+
+### Available tools
+
+| Method | Return value | Description |
+| --- | --- | --- |
+| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | Get context actions, platform APIs, plugin tools, and MCP tools callable in this invocation |
+| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | Call a tool from that list |
+
+Each item returned by `get_available_tools()` contains `name`, `description`, `parameters`, `type`, and `operations`. The result is already filtered by the current Agent or plugin-processor configuration and the event's capabilities. `ctx.call_tool()` cannot call a tool absent from this list.
+
+### Box and attachments
+
+The Runner owns enablement, acquisition, and reuse policy. Use the [LangBot Box APIs](/en/plugin/dev/apis/common#box-sandbox) for resources and these context methods for the current run.
+
+| Method | Returns | Behavior |
+| --- | --- | --- |
+| `await ctx.bind_box(box_id)` | `BoxBinding` | Bind a Workspace Box; returns `box_id`, `outbox`. A run cannot switch Boxes |
+| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | Import selected input `ref` values, or all inputs; returns sandbox paths |
+| `await ctx.export_box_files()` | `list[BoxFile]` | Export this run's outbox as file handles; sends no message |
+| `await ctx.reply_files(file_ids)` | `Any` | Reply with exported IDs, subject to event reply authorization |
+
+`BoxFile` contains `id`, `name`, `type`, `size`, and optional `path`. Original attachment URLs and content remain available. Files are staged only on explicit import. Output handles belong to this run and cannot be sent twice.
+
+Bind before native sandbox tools or file operations. Runs sharing a Box still have separate attachment directories. `ctx.variables` contains public request variables for computing reuse keys.
+
+`ctx.delivery.automatic_reply` is `True` for Pipeline execution: return files using `RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])`. Agents and plugin processors explicitly call `ctx.reply_files()`.
+
+External runners use the same flow through `AgentRunExternalTools`: `langbot_get_box_status`, `langbot_list_boxes`, `langbot_acquire_box`, `langbot_bind_box`, `langbot_import_box_attachments`, and `langbot_export_box_files`. `langbot_reply_files` is available when the run has event reply permission. All calls retain the current run authorization.
+
+### Prompt, history, and events
+
+| Method | Return value | Availability field |
+| --- | --- | --- |
+| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` |
+| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` |
+| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` |
+| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` |
+| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` |
+| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` |
+
+### State
+
+| Method | Return value | Description |
+| --- | --- | --- |
+| `await ctx.state_get(scope, key)` | `dict[str, Any]` | Read a state value |
+| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | Store a JSON-serializable state value |
+| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | Delete a state value |
+| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | List state keys in a scope |
+
+`scope` can be `conversation`, `actor`, `subject`, or `runner`. These methods require `ctx.context.available_apis.state` to be `true`.
+
+### Run records
+
+| Method | Return value | Availability field |
+| --- | --- | --- |
+| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` |
+| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` |
+| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` |
+| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` |
+| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` |
+| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` |
+
+`run_id=None` refers to the current run. The SDK completes a normal event handler automatically. When a custom `run(ctx)` yields `RunnerResult`, do not register the same result again with `run_append_result()`.
+
+### Availability and authorization
+
+Use `ctx.context.available_apis` to check whether prompt, history, event, state, and run-record methods are available. Authorized models, tools, and knowledge bases are in `ctx.resources.models`, `ctx.resources.tools`, and `ctx.resources.knowledge_bases`.
+
+During Runner execution, model, tool, knowledge-base, storage, and platform calls through `self.plugin` are automatically associated with the current run and validated against `ctx.resources`. Do not pass `run_id`. After the handler returns, do not use this context or its run-bound calls from background tasks.
+
+`ctx.api` is the lower-level run-scoped proxy. Normal component code should prefer the `ctx` methods listed on this page and use `self.plugin` for LangBot APIs.
+
+## Data-only contexts
+
+The following components receive task input rather than an EventListener or Runner execution context. Their fields, return values, and complete examples live in the corresponding component guides.
+
+| Component entry point | Main data | Component guide |
+| --- | --- | --- |
+| `KnowledgeEngine.ingest(context: IngestionContext)` | File, target knowledge base, creation settings, and parse result | [KnowledgeEngine](/en/plugin/dev/components/knowledge-engine#document-ingestion) |
+| `KnowledgeEngine.retrieve(context: RetrievalContext)` | Query, target knowledge base, retrieval settings, and filters | [KnowledgeEngine](/en/plugin/dev/components/knowledge-engine#knowledge-retrieval) |
+| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | Query and external knowledge-base settings | [KnowledgeRetriever](/en/plugin/dev/components/knowledge-retriever#retrieval-context) |
+| `Parser.parse(context: ParseContext)` | File content, filename, MIME type, and metadata | [Parser](/en/plugin/dev/components/parser#parse-method) |
+| `Page.handle_api(request: PageRequest)` | Endpoint, HTTP method, body, and headers | [Page](/en/plugin/dev/components/page#pagerequest-fields) |
diff --git a/en/plugin/dev/apis/common.mdx b/en/plugin/dev/apis/common.mdx
index c2d247131..e1196cd9d 100644
--- a/en/plugin/dev/apis/common.mdx
+++ b/en/plugin/dev/apis/common.mdx
@@ -1,116 +1,15 @@
---
-title: "Plugin Common APIs"
+title: "LangBot API"
---
-LangBot provides a series of APIs for plugins to operate various LangBot modules and control message contexts.
-
-## Request API
-
-Operations for the currently processing user request (message). Only available in `EventListener` and `Command` components. Access methods:
-
-- In event handler methods of `EventListener`: Internal methods of `event_context: context.EventContext` object
-- In subcommand handler methods of `Command`: Internal methods of `context: ExecuteContext` object
-
-### Direct Reply Message
-
-Directly reply with a message chain to the session where the current request is located.
-
-For message chain construction methods, please refer to [Message Platform Entities](/en/plugin/dev/apis/messages).
-
-```python
-async def reply(
- self, message_chain: platform_message.MessageChain, quote_origin: bool = False
-):
- """Reply to the message request
-
- Args:
- message_chain (platform.types.MessageChain): LangBot message chain
- quote_origin (bool): Whether to quote the original message
- """
-
-# Usage example
-await event_context.reply(
- platform_message.MessageChain([
- platform_message.Plain(text="Hello, world!"),
- ]),
-)
-```
-
-### Get Bot UUID
-
-Get the UUID of the bot that originated the current request.
-
-```python
-async def get_bot_uuid(self) -> str:
- """Get the bot uuid"""
-
-# Usage example
-bot_uuid = await event_context.get_bot_uuid()
-```
-
-### Set Request Variables
-
-Some information in a single request is stored in request variables. When using external Agent platforms like Dify, [these variables are explicitly passed to Agent platforms](/en/deploy/pipelines/readme.html#request-variables).
-
-```python
-async def set_query_var(self, key: str, value: Any):
- """Set a query variable"""
-
-# Usage example
-await event_context.set_query_var("key", "value")
-```
-
-### Get Request Variables
-
-Get a single request variable.
-
-```python
-async def get_query_var(self, key: str) -> Any:
- """Get a query variable"""
-
-# Usage example
-query_var = await event_context.get_query_var("key")
-```
-
-### Get All Request Variables
-
-```python
-async def get_query_vars(self) -> dict[str, Any]:
- """Get all query variables"""
-
-# Usage example
-query_vars = await event_context.get_query_vars()
-```
-
-### List Pipeline Knowledge Bases
-
-Get the list of knowledge bases configured for the pipeline used by the current request.
-
-```python
-async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]:
- """List knowledge bases configured for the current pipeline"""
-
-# Usage example
-knowledge_bases = await event_context.list_pipeline_knowledge_bases()
-
-# Return example
-[
- {
- "uuid": "kb_uuid",
- "name": "Product Docs",
- "description": "Internal product documentation",
- }
-]
-```
-
-Only returns knowledge bases bound in the `Local Agent` configuration of the pipeline corresponding to the current request. If the current pipeline has no knowledge bases configured, or does not use `Local Agent`, an empty list is returned.
+Use `self.plugin` for models, tools, knowledge bases, and storage. For the current event, replies, and conversation state, see [Context APIs](/en/plugin/dev/apis/agent-run).
## LangBot API
These APIs can be called in any plugin component. Access methods:
- In the plugin root directory `main.py`: Internal methods of the `self` object, these APIs are all provided by the plugin class parent class `BasePlugin`.
-- In any plugin component class: Internal methods of the `self.plugin` object.
+- In regular plugin component classes: Internal methods of the `self.plugin` object.
### Get Plugin Configuration
@@ -224,6 +123,9 @@ async def invoke_llm(
messages: list[provider_message.Message],
funcs: list[resource_tool.LLMTool] = [],
extra_args: dict[str, Any] = {},
+ timeout: float | None = None,
+ *,
+ reasoning_level: ReasoningLevel | None = None,
) -> provider_message.Message:
"""Invoke an LLM model"""
@@ -236,6 +138,65 @@ llm_message = await self.plugin.invoke_llm(
)
```
+
+### Reasoning level
+
+All model invocation methods above accept the optional keyword-only `reasoning_level` parameter:
+
+```python
+response = await self.plugin.invoke_llm(
+ llm_model_uuid=model_uuid,
+ messages=[provider_message.Message(role="user", content="Analyze this problem")],
+ reasoning_level="medium",
+)
+```
+
+Values are `provider_default`, `disabled`, `enabled`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Available levels depend on the model. LangBot validates support and translates the level into provider parameters; unsupported levels raise an error.
+
+- Omitted or `None`: preserves existing model configuration and `extra_args` behavior.
+- `provider_default`: adds no explicit reasoning level.
+- Other levels: apply to this call only. Do not also supply provider-specific reasoning parameters through `extra_args`.
+
+Runners read their own configuration and explicitly pass the appropriate level for each call, including fallback models and tool follow-ups. LangBot does not extract reasoning levels from Runner configuration. Existing plugins require no changes; when an explicit option is used with an unsupported Host, the SDK requests an upgrade before invoking the model.
+
+### Model streaming and usage
+
+| Method | Returns |
+| --- | --- |
+| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None, *, reasoning_level=None)` | `LLMInvokeResult` |
+| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | Async iterator of `MessageChunk` |
+| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | Async iterator of `LLMStreamEvent` |
+
+
+Parameters match `invoke_llm`. `timeout` is in seconds; non-streaming calls default to 120 seconds. Use `invoke_llm_with_usage` when token usage is needed:
+
+```python
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+result = await self.plugin.invoke_llm_with_usage(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+)
+print(result.message.content)
+if result.usage is not None:
+ print(result.usage.total_tokens)
+```
+
+`usage` may be `None`; `prompt_tokens`, `completion_tokens`, and `total_tokens` may also be absent. Missing values do not mean zero usage. Additional provider fields, such as cache usage, are preserved.
+
+```python
+async for event in self.plugin.invoke_llm_stream_events(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+):
+ if event.chunk is not None:
+ print(event.chunk.content)
+ if event.usage is not None:
+ print(event.usage.model_dump())
+```
+
+`LLMStreamEvent` has optional `chunk` and `usage`; the final event may contain usage without text. `invoke_llm_stream` yields only message/tool-call chunks. `MessageChunk.content` is the current chunk; `all_content`, when supplied, is accumulated text. Replace with accumulated text and append only deltas; an `is_final` chunk may still carry text. Model streaming does not send platform messages; see [Run Context APIs](/en/plugin/dev/apis/agent-run) for explicit streaming replies.
+
### List Available Parsers
List Parser plugins currently available on the host, optionally filtered by MIME type.
@@ -428,7 +389,7 @@ Use this in conjunction with configuration fields of type [``file or array[file`
## Knowledge Base API
-These APIs are accessible via `self.plugin` in any component, allowing you to list and retrieve from all knowledge bases in the LangBot instance without pipeline restrictions.
+These APIs are accessible via `self.plugin` in regular components, allowing you to list and retrieve from all knowledge bases in the LangBot instance without pipeline restrictions.
### List All Knowledge Bases
@@ -486,7 +447,7 @@ for entry in results:
```
-These APIs do not require a `query_id` and can be used in any component (including Tool components). They can access all knowledge bases without being restricted to the current pipeline's configuration.
+These APIs do not require a `query_id` and can be used in regular components (including Tool components). They can access all knowledge bases without being restricted to the current pipeline's configuration.
## RAG API
@@ -676,3 +637,95 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
# Usage example
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
```
+
+
+## Additional resource APIs
+
+These methods are also called through `self.plugin`.
+
+### Invoke a rerank model
+
+```python
+async def invoke_rerank(
+ self, rerank_model_uuid: str, query: str, documents: list[str],
+ top_k: int | None = None, extra_args: dict[str, Any] | None = None,
+ timeout: float = 60.0,
+) -> list[dict[str, Any]]:
+ ...
+
+scores = await self.plugin.invoke_rerank(
+ rerank_model_uuid=rerank_model_uuid,
+ query="How do I configure a bot?",
+ documents=["Bot configuration guide", "Plugin publishing guide"],
+ top_k=1,
+)
+```
+
+Results usually contain the original document `index` and `relevance_score`.
+
+### Invoke a parser
+
+```python
+async def invoke_parser(
+ self, plugin_author: str, plugin_name: str, storage_path: str,
+ mime_type: str, filename: str, metadata: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ ...
+
+parsers = await self.plugin.list_parsers(mime_type="application/pdf")
+if parsers:
+ parser = parsers[0]
+ parsed = await self.plugin.invoke_parser(
+ plugin_author=parser["plugin_author"],
+ plugin_name=parser["plugin_name"],
+ storage_path=storage_path,
+ mime_type="application/pdf",
+ filename="guide.pdf",
+ )
+```
+
+`storage_path` is a Host storage path, such as `context.file_object.storage_path` during knowledge ingestion, not a plugin-local path. The result dictionary contains `text`, `sections`, and `metadata`.
+
+### List vector records
+
+```python
+async def vector_list(
+ self, collection_id: str, filters: dict[str, Any] | None = None,
+ limit: int = 20, offset: int = 0,
+) -> dict[str, Any]:
+ ...
+
+page = await self.plugin.vector_list(
+ collection_id=collection_id,
+ filters={"file_id": "doc_001"},
+ limit=20,
+ offset=0,
+)
+for item in page["items"]:
+ print(item["id"], item.get("metadata"))
+```
+
+Returns `items` and `total`. Items contain `id`, `document`, and `metadata`; `total` is a best-effort matching count from the storage backend. Filters use the vector API format above.
+
+### List plugins and commands
+
+```python
+manifests = await self.plugin.list_plugins_manifest()
+commands = await self.plugin.list_commands()
+```
+
+Both methods take no arguments and return the Host-provided plugin manifest list and command list respectively.
+
+### Box sandbox
+
+These methods use `self.plugin` and the current Workspace. Runner calls automatically retain invocation authorization.
+
+| Method | Returns | Behavior |
+| --- | --- | --- |
+| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`, `available`, capacity `limit`, `used`, `remaining`, optional managed `required_reuse_key`, and unavailable `reason` |
+| `await self.plugin.list_boxes()` | `list[BoxSession]` | Box `id` and `status` snapshot (`idle`, `running`, `closing`) |
+| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | Reuse the same key or create within capacity; `options` supports `image` |
+
+Unknown capacity fields are `None`. Zero remaining capacity still permits reuse; Runtime checks creation capacity atomically. The Runner computes reuse keys. When `required_reuse_key` is supplied, use it. Equal keys in different Workspaces never share a Box.
+
+Use the [Runner context](/en/plugin/dev/apis/agent-run#runner) to bind the Box before importing attachments or executing tools.
diff --git a/en/plugin/dev/apis/messages.mdx b/en/plugin/dev/apis/messages.mdx
index eadf571c6..e4a084016 100644
--- a/en/plugin/dev/apis/messages.mdx
+++ b/en/plugin/dev/apis/messages.mdx
@@ -58,34 +58,8 @@ When creating components, named parameters must be used. For specific usage, you
## Other Entities
-In addition to the above entities, there are also entities like `Friend` and `Group`. These entities are defined in `langbot_plugin.api.entities.platform.entities`.
+In addition to the above entities, there are also entities like `Friend` and `Group`. These entities are defined in `langbot_plugin.api.entities.builtin.platform.entities`.
-## Accessing Underlying Platform APIs
+## Platform APIs
-
- Not applicable for 4.x.
-
-
-To abstract away platform differences, LangBot provides an abstraction layer above messaging platform APIs, which includes the content described above and various platform adapters in the `pkg/platform/sources` directory. However, due to significant platform differences, the entity and API encapsulation in the abstraction layer is not complete. If your plugin needs to access platform-specific functionality, you can access underlying APIs according to the following instructions.
-
-
- ```python
- # For example in a plugin
- @handler(GroupMessageReceived)
- async def _(self, ctx: EventContext):
- # Get adapter object
- adapter = ctx.query.adapter
- ```
-
-
-| Platform | Adapter | Access Method | Description |
-| ------------------------- | --------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| OneBot v11 | aiocqhttp | adapter.bot | bot object corresponds to [aiocqhttp](https://github.com/nonebot/aiocqhttp) CQHttp object, please refer to aiocqhttp documentation for detailed usage |
-| QQ Official API | qqofficial | adapter.bot | bot object is the SDK object under libs/qq_official_api, for detailed usage please refer to the `send_group_text_msg` method in `QQOfficialClient` in libs/qq_official_api/api.py for HTTP request methods, please refer to QQ Official API documentation for specific interface documentation |
-| WeCom (Enterprise WeChat) | wecom | adapter.bot | Refer to the `send_image` method in `WecomClient` in libs/wecom_api/api.py for HTTP request methods, please refer to WeCom API documentation for specific interface documentation |
-| WeChat Official Account | officialaccount | - | Please refer to WeChat Official Account API documentation for interface documentation, credential information can be obtained from adapter.bot |
-| Lark | lark | adapter.api_client | Lark SDK's API Client object, please refer to [oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) for details |
-| DingTalk | dingtalk | - | Please refer to DingTalk API documentation, credential information can be obtained from adapter.bot |
-| Discord | discord | adapter.bot | bot object is [Rapptz/discord.py](https://github.com/Rapptz/discord.py) Discord object, please refer to documentation for detailed usage |
-| Telegram | telegram | adapter.bot | bot object is [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) Telegram object, please refer to documentation for detailed usage |
-| Slack | slack | adapter.bot | bot object in libs/slack_api/api.py, please refer to its code and Slack SDK for detailed usage |
\ No newline at end of file
+Use [Platform APIs](/en/plugin/dev/apis/platform) to query users, manage groups, and call platform-specific actions. Runner uses the same `self.plugin` entrypoint; use `ctx.reply()` for the current conversation.
diff --git a/en/plugin/dev/apis/pipeline-events.mdx b/en/plugin/dev/apis/pipeline-events.mdx
index 040615e69..50183854f 100644
--- a/en/plugin/dev/apis/pipeline-events.mdx
+++ b/en/plugin/dev/apis/pipeline-events.mdx
@@ -248,42 +248,13 @@ class PromptPreProcessing(BaseEventModel):
"""Existing message records for this conversation, can be modified, langbot_plugin.api.entities.builtin.provider.message.Message or langbot_plugin.api.entities.builtin.provider.message.MessageChunk type"""
```
-## Event Context APIs
+## EventListener context
-```python
-...
- @self.handler(events.PersonMessageReceived)
- async def handler(event_context: context.EventContext):
- ...
-```
+Event handlers read the current event through `event_context.event`. See [Context APIs](/en/plugin/dev/apis/agent-run#eventlistener) for the complete `EventContext` properties and methods.
-Event processing methods will be passed the `EventContext` object, which contains event context information, and the object has both [Request API](/en/plugin/dev/apis/common) and event context specific APIs. The following is a list of event context specific APIs:
+### Prevent default behavior
-### Get Original Event Attributes
-
-`event_context.event` attribute is the original event object, which can get the attributes of the original event. The type is the type of the event being listened to.
-
-For example, when the event being listened to is `GroupMessageReceived`:
-
-```python
-event = event_context.event
-print(event.launcher_type) # Launcher object type (group)
-print(event.launcher_id) # Group ID
-print(event.sender_id) # Sender account ID
-print(event.message_chain) # Message chain
-```
-
-### Prevent Default Behavior
-
-```python
-def prevent_default(self):
- """Prevent default behavior"""
-
-# Usage example
-event_context.prevent_default()
-```
-
-Calling this method will prevent the default subsequent behavior of this event, and the pipeline will end directly.
+Calling `event_context.prevent_default()` stops the default handling of the current event. Only these events support it:
Only the following events can prevent default behavior:
@@ -297,14 +268,4 @@ Only the following events can prevent default behavior:
- NormalMessageResponded
-### Prevent Postorder Execution
-
-```python
-def prevent_postorder(self):
- """Prevent postorder execution"""
-
-# Usage example
-event_context.prevent_postorder()
-```
-
-Calling this method will prevent the subsequent plugins from executing this time.
\ No newline at end of file
+Call `event_context.prevent_postorder()` to stop later plugins from handling the current event.
diff --git a/en/plugin/dev/apis/platform.mdx b/en/plugin/dev/apis/platform.mdx
new file mode 100644
index 000000000..40219c03f
--- /dev/null
+++ b/en/plugin/dev/apis/platform.mdx
@@ -0,0 +1,78 @@
+---
+title: "Platform APIs"
+---
+
+Call platform adapter APIs using a bot UUID. Continue using `self.plugin.send_message()` for message chains; use the API below for groups, users, and other platform features.
+
+## Call a platform API
+
+```python
+async def call_platform_api(
+ self,
+ bot_uuid: str,
+ action: str,
+ params: dict[str, Any] | None = None,
+) -> Any:
+ ...
+```
+
+Call through `self.plugin` in Command, EventListener, Tool, and other regular components. Results are serialized dictionaries, lists, scalars, or `None`, depending on the action, rather than platform SDK objects.
+
+```python
+group = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="get_group_info",
+ params={"group_id": "123456"},
+)
+```
+
+`bot_uuid` comes from `await self.plugin.get_bots()` or `await event_context.get_bot_uuid()` in a Pipeline event. Use platform IDs from real events or platform configuration.
+
+### Common actions
+
+| `action` | `params` |
+| --- | --- |
+| `get_user_info` | `user_id` |
+| `get_friend_list` | `{}` |
+| `get_group_info` | `group_id` |
+| `get_group_list` | `{}` |
+| `get_group_member_list` | `group_id` |
+| `get_group_member_info` | `group_id`, `user_id` |
+| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` |
+| `set_group_name` | `group_id`, `name` |
+| `mute_member` | `group_id`, `user_id`, `duration` |
+| `unmute_member` / `kick_member` | `group_id`, `user_id` |
+| `leave_group` | `group_id` |
+| `approve_friend_request` | `request_id`, `approve`, `remark` |
+| `approve_group_invite` | `request_id`, `approve` |
+| `get_file_url` | `file_id` |
+
+
+Supported actions vary by platform and may require bot administrator permissions. `duration` is in seconds; the meaning of `0` is platform-specific, not a universal permanent mute. Queries may depend on platform caches. Calls raise an exception if the bot is not running, the action is unsupported, or parameters are invalid.
+
+## Platform-specific APIs
+
+Set the outer `action` to `call_platform_api` and put the platform action and parameters inside `params`. For example, read the bot account using OneBot Omni:
+
+```python
+account = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="call_platform_api",
+ params={"action": "get_login_info", "params": {}},
+)
+```
+
+Only actions declared by the adapter are allowed; this is not unrestricted pass-through to every platform endpoint. Plugins call the Host through the runtime, rather than accessing `ctx.query.adapter` or `adapter.bot`.
+
+## Runner
+
+During Runner execution, model, tool, knowledge-base, storage, and platform calls through `self.plugin` automatically use the current run and its authorization. Do not pass `run_id`. Concurrent runs remain isolated. Do not continue these run-bound calls after the handler returns.
+
+Use `await ctx.get_bot_uuid()` for the current bot. Platform calls must match the tool grants for this invocation. Event-target grants cannot access another target. Ungranted actions, including platform-specific actions, are rejected. These calls also use Mock delivery in debug mode.
+
+```python
+tools = await ctx.get_available_tools()
+if any(tool["name"] == "event_get_actor" for tool in tools):
+ actor = await ctx.call_tool("event_get_actor")
+ await ctx.log(str(actor))
+```
diff --git a/en/plugin/dev/basic-info.mdx b/en/plugin/dev/basic-info.mdx
index e51fc5a3b..b7b8c4df2 100644
--- a/en/plugin/dev/basic-info.mdx
+++ b/en/plugin/dev/basic-info.mdx
@@ -159,6 +159,7 @@ Dropdown menu. Requires configuring `options`, which represents the dropdown men
```yaml
- name: mode
type: select
+ allow_custom: true
options: # Dropdown options, supports multilingual.
- name: mode1 # Value, displayed as secondary text in the dropdown.
label: # Display name, supports multilingual.
@@ -171,6 +172,8 @@ Dropdown menu. Requires configuring `options`, which represents the dropdown men
...
```
+Set `allow_custom: true` to add a Custom option alongside the presets. A text input appears only when Custom is selected. The saved value is the selected `name` or the custom string, never a Custom marker. This also applies to a runner’s `spec.config`.
+
### type: prompt-editor
Prompt editor. Displays a prompt editor from the pipeline configuration page, with the final result represented in OpenAI's `messages` format.
diff --git a/en/plugin/dev/components/add.mdx b/en/plugin/dev/components/add.mdx
index 4bc82626a..869d1b5f4 100644
--- a/en/plugin/dev/components/add.mdx
+++ b/en/plugin/dev/components/add.mdx
@@ -4,6 +4,8 @@ title: "Adding Components"
Plugins consist of one or more components that provide different functionalities to LangBot. Currently supported component types include:
+- Plugin processor (Runner): handles platform events in plugin code after an instance is created and bound. See the [tutorial](/en/plugin/dev/components/runner).
+- Agent runner (Runner): provides execution logic selected in an Agent configuration; it is distinct from a standalone plugin processor.
- Event Listeners (EventListener): Listen to events during pipeline execution and modify context or pipelines.
- Commands (Command): Triggered by user command messages starting with `!` (or other configured prefixes).
- Tools (Tool): Called by LLMs during execution of LangBot's built-in Local Agent.
@@ -70,6 +72,7 @@ To remove components, you can delete the corresponding information manually.
For detailed usage of each component, please refer to:
+- [Runner](./runner)
- [EventListener](./event-listener)
- [Command](./command)
- [Tool](./tool)
diff --git a/en/plugin/dev/components/command.mdx b/en/plugin/dev/components/command.mdx
index c9fd77d87..5e6d03787 100644
--- a/en/plugin/dev/components/command.mdx
+++ b/en/plugin/dev/components/command.mdx
@@ -211,4 +211,5 @@ For specific return values, please refer to the definition of `CommandReturn`: [
You have learned the basic information about command registration and command execution. Next, you can:
-- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
+- See the complete [`ExecuteContext` API reference](/en/plugin/dev/apis/agent-run#command).
+- Check out [Plugin Common APIs](/en/plugin/dev/apis/common).
diff --git a/en/plugin/dev/components/event-listener.mdx b/en/plugin/dev/components/event-listener.mdx
index e36937c42..298cfd1f7 100644
--- a/en/plugin/dev/components/event-listener.mdx
+++ b/en/plugin/dev/components/event-listener.mdx
@@ -2,6 +2,10 @@
title: "Component: Event Listener"
---
+
+This component extends Pipeline processing steps. To handle Bot events such as incoming messages and new group members, see [Event Processor](/en/plugin/dev/components/runner).
+
+
LangBot's pipeline generates events during runtime for plugins to hook into and process. Each plugin can only have one event listener (components/event_listener/default.yaml), but can register any number of events within it.
## Adding Event Listener Component
@@ -99,5 +103,6 @@ Event registration is implemented through the `@self.handler` decorator, with th
You have learned the basic information about event listeners. Next, you can:
-- Check the `List of Monitorable Events` and `Event Context APIs` in the documentation [Pipeline Events](/en/plugin/dev/apis/pipeline-events)
+- See [Pipeline Events](/en/plugin/dev/apis/pipeline-events) for event types and fields.
+- See the complete [`EventContext` API reference](/en/plugin/dev/apis/agent-run#eventlistener).
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
diff --git a/en/plugin/dev/components/runner.mdx b/en/plugin/dev/components/runner.mdx
new file mode 100644
index 000000000..688a014fb
--- /dev/null
+++ b/en/plugin/dev/components/runner.mdx
@@ -0,0 +1,199 @@
+---
+title: "Component: Runner"
+---
+
+Runner implements Agent execution or typed event handlers for a **Plugin processor**. Both styles receive `RunnerContext`. After installation, users select and configure the component in LangBot.
+
+For environment setup, see the [plugin development tutorial](/en/plugin/dev/tutor).
+
+See [Context APIs](/en/plugin/dev/apis/agent-run#runner) for the complete `RunnerContext` fields and methods.
+
+## Adding a Runner Component
+
+A plugin can contain several event processors. Run `lbp comp Runner` in the plugin directory. Enter `welcome` as the name and `Welcome new members` as the description.
+
+```bash
+lbp comp Runner
+```
+
+The generated `components/runner/welcome.yaml` defines the component, and `welcome.py` contains its implementation.
+
+## Custom execution
+
+To integrate a model or implement an execution loop, declare `spec.usages: [agent]` and yield results from `run(ctx)`:
+
+```python
+from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+
+class Echo(Runner):
+ async def run(self, ctx: RunnerContext):
+ yield RunnerResult.run_completed(
+ ctx.run_id,
+ message=Message(role="assistant", content=ctx.input.to_text()),
+ )
+```
+
+## Manifest File: Runner
+
+Replace `welcome.yaml` with:
+
+```yaml
+apiVersion: langbot/v1
+kind: Runner
+metadata:
+ name: welcome
+ label:
+ en_US: Welcome members
+ zh_Hans: 欢迎新成员
+ ja_JP: 新しいメンバーを歓迎
+ description:
+ en_US: Welcome new members and respond to /hello.
+ zh_Hans: 欢迎新成员,并响应 /hello 消息。
+ ja_JP: 新しいメンバーを歓迎し、/hello に返信します。
+spec:
+ usages: [event]
+ events:
+ - group.member_joined
+ - message.received
+ capabilities:
+ tool_calling: true
+ permissions:
+ tools: [detail, call]
+ config:
+ - name: greeting
+ type: string
+ label:
+ en_US: Greeting
+ zh_Hans: 欢迎语
+ ja_JP: 歓迎メッセージ
+ required: true
+ default: Welcome aboard!
+ - name: reply_enabled
+ type: boolean
+ label:
+ en_US: Send replies
+ zh_Hans: 发送回复
+ ja_JP: 返信する
+ default: true
+execution:
+ python:
+ path: ./welcome.py
+ attr: Welcome
+```
+
+`spec.events` lists supported events, and `spec.config` defines settings users can edit on the processor detail page. This example handles new members and incoming messages, with a greeting and a reply switch. Sending replies requires the `tool_calling` and `permissions.tools` settings shown above.
+
+
+`spec.usages` is required and has no default. `spec.usages` controls where the component can be selected: `agent` for Agents and Pipelines, `event` for Plugin processors. Both can be declared as `[agent, event]`. Event usage requires `spec.events`. The example uses typed handlers; a custom `run(ctx)` can also call `await super().run(ctx)` to dispatch them.
+
+## Plugin Implementation
+
+Register handlers in the `Welcome` class's `initialize` method. Replace `welcome.py` with:
+
+```python
+from langbot_plugin.api.definition.components.runner import (
+ Runner,
+ RunnerContext,
+)
+from langbot_plugin.api.entities.builtin.platform.events import (
+ MemberJoinedEvent,
+ MessageReceivedEvent,
+)
+from langbot_plugin.api.entities.builtin.platform.message import Plain
+
+
+class Welcome(Runner):
+ async def initialize(self):
+ await super().initialize()
+
+ @self.handler(MemberJoinedEvent)
+ async def on_join(ctx: RunnerContext):
+ name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id)
+ await ctx.log(f"Member joined: {name}")
+ if not ctx.config.get("reply_enabled", True):
+ await ctx.log("Replies are disabled for this processor")
+ return
+ greeting = ctx.config.get("greeting", "Welcome aboard!")
+ await ctx.reply(f"{name}, {greeting}")
+ await ctx.log("Welcome action completed")
+
+ @self.handler(MessageReceivedEvent)
+ async def on_message(ctx: RunnerContext):
+ text = "".join(
+ item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain)
+ ).strip()
+ if text != "/hello":
+ await ctx.log("Message ignored: expected /hello")
+ return
+ if ctx.config.get("reply_enabled", True):
+ await ctx.reply(ctx.config.get("greeting", "Welcome aboard!"))
+ else:
+ await ctx.log("Replies are disabled for this processor")
+```
+
+`on_join` welcomes new members, and `on_message` replies to `/hello`. With **Send replies** turned off, they only write logs.
+
+`ctx.platform_event` contains the event data, and `ctx.config` contains this processor's settings. Use `ctx.reply()` to reply and `ctx.log()` to write logs. Processing finishes when the handlers return.
+
+## Streaming Replies
+
+Use `ctx.reply_stream()` to update one reply. `update(text)` takes the complete text so far:
+
+```python
+async with ctx.reply_stream() as reply:
+ await reply.update("Working…")
+ await reply.update("Done.")
+```
+
+The reply finishes when the `async with` block exits normally. If streaming is unsupported, disabled, or the event is not a message, the Host sends one complete message at the end. Exceptions discard buffered partial text. Debug runs simulate delivery.
+
+
+## Registering Events
+
+Register a handler with `@self.handler(EventClass)` and add the event identifier to `spec.events`. For example, `MemberJoinedEvent` corresponds to `group.member_joined`.
+
+| Event | Manifest identifier | SDK event class |
+| --- | --- | --- |
+| Message received | `message.received` | `MessageReceivedEvent` |
+| Message edited | `message.edited` | `MessageEditedEvent` |
+| Message deleted | `message.deleted` | `MessageDeletedEvent` |
+| Message reaction | `message.reaction` | `MessageReactionEvent` |
+| Feedback received | `feedback.received` | `FeedbackReceivedEvent` |
+| Member joined | `group.member_joined` | `MemberJoinedEvent` |
+| Member left | `group.member_left` | `MemberLeftEvent` |
+| Member muted | `group.member_banned` | `MemberBannedEvent` |
+| Group information updated | `group.info_updated` | `GroupInfoUpdatedEvent` |
+| Friend request received | `friend.request_received` | `FriendRequestReceivedEvent` |
+| Friend added | `friend.added` | `FriendAddedEvent` |
+| Friend removed | `friend.removed` | `FriendRemovedEvent` |
+| Bot invited to group | `bot.invited_to_group` | `BotInvitedToGroupEvent` |
+| Bot removed from group | `bot.removed_from_group` | `BotRemovedFromGroupEvent` |
+| Bot muted | `bot.muted` | `BotMutedEvent` |
+| Bot unmuted | `bot.unmuted` | `BotUnmutedEvent` |
+| Platform-specific event | `platform.specific` | `PlatformSpecificEvent` |
+
+See the SDK's [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py) for available events and fields. Event support varies by platform.
+
+## Testing the Event Processor
+
+Configure the debug connection following the [plugin development tutorial](/en/plugin/dev/tutor), then run `lbp run` in the plugin directory. In LangBot:
+
+1. Create a **Plugin processor**, select **Welcome members** on its detail page, configure it, and save.
+2. Select **Member joined** on the left, enter the member nickname, member ID, and group ID, then click **Run test**.
+3. Inspect the reply and logs. You can also select **Message received** and enter `/hello` to test replies.
+
+Platform replies use Mock during debugging and send no real messages. Restart `lbp run` after editing component YAML.
+
+After installing the plugin, add this configuration under **Plugin processor** on the bot detail page and save. You can also select a component, fill in its configuration, and create and bind a new configuration directly there. Declared events are delivered automatically; no per-event routes are needed.
+
+Plugin subscriptions run independently of Agent/Pipeline routes. An event can trigger multiple processors, and one failure does not prevent the others from running. Bots using the same configuration share settings and runtime state; create separate configurations for different settings. Expose optional behavior, such as replies, through component configuration to avoid duplicate responses.
+
+## Next Steps
+
+- See the [`RunnerContext` API reference](/en/plugin/dev/apis/agent-run#runner).
+- See [LangBot API](/en/plugin/dev/apis/common) for models, tools, knowledge bases, and storage.
+- See [Platform API](/en/plugin/dev/apis/platform) for messaging and other platform operations.
+- Explore more examples in [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo).
+- To extend Pipeline steps such as preprocessing or completion of a model call, use [Event Listener](/en/plugin/dev/components/event-listener).
diff --git a/en/plugin/dev/components/tool.mdx b/en/plugin/dev/components/tool.mdx
index f1ff5ccce..5ac07dc85 100644
--- a/en/plugin/dev/components/tool.mdx
+++ b/en/plugin/dev/components/tool.mdx
@@ -173,4 +173,5 @@ Now execute the command `lbp run` in the plugin directory to start debugging. Co
You have learned the basic usage of tools. Next, you can:
+- See how Tool receives [`session` and `query_id`](/en/plugin/dev/apis/agent-run#tool).
- Check out [Plugin Common APIs](/en/plugin/dev/apis/common)
diff --git a/en/plugin/dev/tutor.mdx b/en/plugin/dev/tutor.mdx
index 21d3261da..9b8e26926 100644
--- a/en/plugin/dev/tutor.mdx
+++ b/en/plugin/dev/tutor.mdx
@@ -2,6 +2,10 @@
title: "Plugin Development Tutorial"
---
+
+To build a plugin for incoming messages, new group members, and other Bot events, complete the basic setup here and continue with the [Event Processor tutorial](/en/plugin/dev/components/runner).
+
+
## Architecture Foundation
In version 4.0, we introduced a high-security, high-flexibility production-grade plugin system and provided developers with rich APIs and easy-to-use supporting tools.
diff --git a/ja/plugin/dev/apis/agent-run.mdx b/ja/plugin/dev/apis/agent-run.mdx
new file mode 100644
index 000000000..bd49ed182
--- /dev/null
+++ b/ja/plugin/dev/apis/agent-run.mdx
@@ -0,0 +1,261 @@
+---
+title: "コンテキスト API"
+---
+
+コンテキストには、1 回のコンポーネント呼び出しに関するデータが含まれます。コンポーネントごとに受け取るコンテキストが異なるため、まず対象コンポーネントの入口を確認してください。
+
+| コンポーネント | ハンドラーの入口 | コンテキスト型 |
+| --- | --- | --- |
+| EventListener | `handler(event_context)` | `EventContext` |
+| Command | `subcommand(context)` | `ExecuteContext` |
+| Tool | `call(params, session, query_id)` | 独立したコンテキストオブジェクトなし |
+| Runner | `run(ctx)` または `handler(ctx)` | `RunnerContext` |
+| KnowledgeEngine | `ingest(context)`、`retrieve(context)` | `IngestionContext`、`RetrievalContext` |
+| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` |
+| Parser | `parse(context)` | `ParseContext` |
+| Page | `handle_api(request)` | `PageRequest` |
+
+このページでは、コンテキストのデータとメソッドのみを説明します。モデル、ナレッジベース、ストレージなどの LangBot 機能には `self.plugin` を使用します。詳細は [LangBot API](/ja/plugin/dev/apis/common) を参照してください。プラットフォーム操作は[プラットフォーム API](/ja/plugin/dev/apis/platform)に記載しています。
+
+`EventContext`、`ExecuteContext`、`Session` には `instance_uuid`、`workspace_uuid`、`placement_generation` も含まれます。これらは現在の実行に対応するインスタンスとワークスペースを示すためのもので、プラグイン側の認可情報として使用しないでください。
+
+## EventListener
+
+EventListener はパイプラインイベントを処理します。イベントハンドラーは `EventContext` を受け取ります。
+
+```python
+event = event_context.event
+sender_id = event.sender_id
+```
+
+### コンテキストデータ
+
+| プロパティ | 型 | 説明 |
+| --- | --- | --- |
+| `event` | `BaseEventModel` | 現在のイベントオブジェクト。実際の型は登録したイベント型と一致します |
+| `event_name` | `str` | 現在のイベントクラス名 |
+| `query_id` | `int` | 現在のパイプラインリクエスト ID |
+| `query_uuid` | `str \| None` | 現在のパイプラインリクエストの安定した識別子 |
+| `eid` | `int` | Plugin Runtime 内の一時的なイベント番号。永続化しないでください |
+| `is_prevent_default` | `bool` | デフォルト処理を停止済みか。変更には `prevent_default()` を使用します |
+| `is_prevent_postorder` | `bool` | 後続プラグインを停止済みか。変更には `prevent_postorder()` を使用します |
+
+イベントのフィールドと購読可能なイベントは[パイプラインイベント](/ja/plugin/dev/apis/pipeline-events)を参照してください。
+
+### コンテキストメソッド
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await event_context.reply(message_chain, quote_origin=False)` | - | 現在のリクエストの会話に返信 |
+| `await event_context.get_bot_uuid()` | `str` | 送信元 Bot の UUID を取得 |
+| `await event_context.set_query_var(key, value)` | - | 現在のリクエストに変数を設定 |
+| `await event_context.get_query_var(key)` | `Any` | リクエスト変数を 1 つ取得 |
+| `await event_context.get_query_vars()` | `dict[str, Any]` | すべてのリクエスト変数を取得 |
+| `await event_context.create_new_conversation()` | `dict[str, Any]` | 現在のパイプライン会話をクリアし、後続リクエストで新しい会話を作成 |
+| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 現在のパイプラインの Local Agent に設定されたナレッジベースを取得 |
+| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 現在のパイプラインに設定されたナレッジベースを検索 |
+| `event_context.prevent_default()` | - | 現在のイベントのデフォルト処理を停止 |
+| `event_context.prevent_postorder()` | - | 後続プラグインによる現在のイベント処理を停止 |
+
+`reply()` は [MessageChain](/ja/plugin/dev/apis/messages) を受け取ります。リクエスト変数、返信、パイプラインのナレッジベース API には、関連付けられたパイプラインリクエストが必要です。`retrieve_knowledge()` の `kb_id` は `list_pipeline_knowledge_bases()` の結果から指定します。
+
+`prevent_default()` はデフォルトフローを停止できるイベントにのみ作用します。詳細は[パイプラインイベント](/ja/plugin/dev/apis/pipeline-events#デフォルト動作を阻止)を参照してください。
+
+## Command
+
+Command のサブコマンドハンドラーは `ExecuteContext` を受け取ります。
+
+```python
+query = " ".join(context.crt_params)
+session = context.session
+```
+
+### コンテキストデータ
+
+| プロパティ | 型 | 説明 |
+| --- | --- | --- |
+| `session` | `Session` | 現在のメッセージが属するセッション |
+| `command_text` | `str` | コマンド接頭辞を除いた完全なコマンド文字列 |
+| `full_command_text` | `str` | 接頭辞を含む完全なコマンド文字列 |
+| `command` | `str` | ルートコマンド名 |
+| `crt_command` | `str` | 現在実行中のサブコマンド名 |
+| `params` | `list[str]` | ルートコマンド以降のすべての引数 |
+| `crt_params` | `list[str]` | 現在のサブコマンドで未処理の引数 |
+| `privilege` | `int` | 現在のユーザーのコマンド権限レベル |
+| `query_id` | `int` | 現在のパイプラインリクエスト ID |
+| `query_uuid` | `str \| None` | 現在のパイプラインリクエストの安定した識別子 |
+
+SDK は現在のサブコマンドに入る前に `context.shift()` を呼び出します。コンポーネントから直接呼び出す必要はありません。
+
+### コンテキストメソッド
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await context.reply(message_chain, quote_origin=False)` | - | 現在のリクエストの会話に返信 |
+| `await context.get_bot_uuid()` | `str` | 送信元 Bot の UUID を取得 |
+| `await context.set_query_var(key, value)` | - | 現在のリクエストに変数を設定 |
+| `await context.get_query_var(key)` | `Any` | リクエスト変数を 1 つ取得 |
+| `await context.get_query_vars()` | `dict[str, Any]` | すべてのリクエスト変数を取得 |
+| `await context.create_new_conversation()` | `dict[str, Any]` | 現在のパイプライン会話をクリアし、後続リクエストで新しい会話を作成 |
+| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 現在のパイプラインの Local Agent に設定されたナレッジベースを取得 |
+| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 現在のパイプラインに設定されたナレッジベースを検索 |
+
+これらのリクエストメソッドは EventListener と同じ動作です。登録方法と戻り値は [Command コンポーネント](/ja/plugin/dev/components/command)を参照してください。
+
+## Tool
+
+Tool には独立したコンテキストオブジェクトがありません。`call()` の引数として呼び出し情報を受け取ります。
+
+```python
+conversation_type = session.launcher_type.value
+conversation_id = session.launcher_id
+sender_id = session.sender_id
+```
+
+| 引数 | 説明 |
+| --- | --- |
+| `params` | ツールの JSON Schema に基づいてモデルまたは呼び出し元が生成した引数 |
+| `session` | 現在のセッションデータ |
+| `query_id` | 現在のパイプラインリクエスト ID。リクエストコンテキストが必要な LangBot API にそのまま渡します |
+
+`Session` の主なプロパティは次のとおりです。
+
+| プロパティ | 説明 |
+| --- | --- |
+| `launcher_type` | 会話種別:`person` または `group` |
+| `launcher_id` | 個人チャットのユーザー ID またはグループ ID |
+| `sender_id` | 現在のメッセージ送信者 ID |
+| `bot_uuid` | 現在の Bot UUID |
+| `using_conversation` | 現在のパイプライン会話 |
+| `conversations` | この Session に保存されたパイプライン会話 |
+| `use_prompt_name` | 使用中のプロンプト名 |
+| `create_time`、`update_time` | Session の作成時刻と更新時刻 |
+
+`session` はデータオブジェクトであり、`reply()` などのコンテキストメソッドはありません。ツールの戻り値は呼び出し元へ返され、チャットプラットフォームには自動送信されません。LangBot とプラットフォームの機能には `self.plugin` を使用します。API が `session` と `query_id` を要求する場合は、現在の呼び出しで受け取った値を渡してください。
+
+## Runner
+
+独自の `run(ctx)` と Runner のイベントハンドラーは、どちらも `RunnerContext` を受け取ります。このコンテキストは現在の実行中だけ有効です。
+
+```python
+text = ctx.input.to_text()
+event_type = ctx.event.event_type
+```
+
+### コンテキストデータ
+
+| プロパティ | 型 | 説明 |
+| --- | --- | --- |
+| `run_id` | `str` | 現在の実行 ID |
+| `trigger` | `AgentTrigger` | トリガーの種類、発生元、時刻 |
+| `event` | `AgentEventContext` | イベント ID、種類、発生元、時刻、元データを含む標準イベントエンベロープ |
+| `platform_event` | プラットフォームイベント型 | イベント型に応じて解析されたプラットフォームイベント |
+| `conversation` | `ConversationContext \| None` | 現在の会話、スレッド、Bot、ワークスペースの識別子 |
+| `actor` | `ActorContext \| None` | イベントを発生させた主体 |
+| `subject` | `SubjectContext \| None` | イベントの対象となるメッセージやグループなど |
+| `input` | `AgentInput` | 現在のテキスト、構造化コンテンツ、添付ファイル |
+| `delivery` | `DeliveryContext` | 出力先とストリーミング、編集、リアクションの対応状況 |
+| `resources` | `AgentResources` | 今回の実行で許可されたモデル、ツール、ナレッジベース、スキル、ストレージ |
+| `context` | `ContextAccess` | 会話カーソル、インライン方針、利用可能なコンテキスト API |
+| `state` | `AgentRunState` | 実行開始時に読み込まれたスコープ別状態のスナップショット |
+| `runtime` | `AgentRuntimeContext` | LangBot バージョン、トレース ID、期限 |
+| `config` | `dict[str, Any]` | 現在の Agent またはプラグインプロセッサーの設定 |
+| `adapter` | `AdapterContext \| None` | エントリーアダプターの追加データ |
+| `variables` | `dict[str, Any]` | Runner テンプレートで使える公開リクエスト変数 |
+| `metadata` | `dict[str, Any]` | ホストが提供するその他のメタデータ |
+
+上表の安定したプロパティを優先してください。`event.data` と `adapter.extra` には、安定したフィールドに含まれない追加データが入ります。
+
+### 返信とログ
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await ctx.get_bot_uuid()` | `str` | 送信元 Bot UUID を取得。関連する Bot がなければエラー |
+| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | 文字列または `MessageChain` で現在のイベントに返信 |
+| `ctx.reply_stream()` | 非同期コンテキストマネージャー | 1 件の返信を完全なテキストで逐次更新 |
+| `await ctx.log(text, level="info")` | - | 今回の実行ログへ記録。レベルは `debug`、`info`、`warning`、`error` |
+
+`reply_stream().update(text)` には、その時点の完全なテキストを渡します。プラットフォームがストリーミングに対応しない場合、終了時に完全なメッセージを 1 件送信します。デバッグモードでは送信をシミュレートします。
+
+### 利用可能なツール
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | 今回呼び出せるコンテキスト操作、プラットフォーム API、プラグインツール、MCP ツールを取得 |
+| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | 上記一覧のツールを呼び出す |
+
+`get_available_tools()` の各項目には `name`、`description`、`parameters`、`type`、`operations` が含まれます。結果は現在の Agent またはプラグインプロセッサーの設定とイベント機能で絞り込まれています。一覧にないツールは `ctx.call_tool()` で呼び出せません。
+
+### Box と添付ファイル
+
+Box の有効化、取得、再利用方針は Runner が決めます。リソースの取得には [LangBot API](/ja/plugin/dev/apis/common#box-サンドボックス)、実行中の操作には以下を使います。
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await ctx.bind_box(box_id)` | `BoxBinding` | Box を現在の実行にバインドし、`box_id`、`outbox` を返す。同じ実行内では変更不可 |
+| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | 入力の `ref` を指定してインポート。省略時は全件。サンドボックスの `path` を返す |
+| `await ctx.export_box_files()` | `list[BoxFile]` | 現在の実行の outbox をファイルハンドルとしてエクスポート。メッセージは送信しない |
+| `await ctx.reply_files(file_ids)` | `Any` | エクスポートした ID で返信。返信ツールの権限制限が適用される |
+
+`BoxFile` は `id`、`name`、`type`、`size`、任意の `path` を含みます。入力の元 URL と内容は保持され、明示的にインポートして初めてファイルパスが生成されます。ハンドルは今回の実行でのみ有効で、重複送信できません。
+
+ネイティブツールやファイル操作の前に Box をバインドします。Box を共有しても添付ディレクトリは実行ごとに分かれます。`ctx.variables` の公開リクエスト変数で再利用キーを計算できます。
+
+Pipeline では `ctx.delivery.automatic_reply` が `True` になり、`RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])` で添付を返せます。Agent とプラグインプロセッサーは `ctx.reply_files()` で明示的に送信します。
+
+外部ランナーも `AgentRunExternalTools` で同じ手順を使用できます:`langbot_get_box_status`、`langbot_list_boxes`、`langbot_acquire_box`、`langbot_bind_box`、`langbot_import_box_attachments`、`langbot_export_box_files`。イベントへの返信権限がある場合は `langbot_reply_files` も利用できます。すべての呼び出しに現在の実行の権限が適用されます。
+
+### プロンプト、履歴、イベント
+
+| メソッド | 戻り値 | 利用可否フィールド |
+| --- | --- | --- |
+| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` |
+| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` |
+| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` |
+| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` |
+| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` |
+| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` |
+
+### 状態
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await ctx.state_get(scope, key)` | `dict[str, Any]` | 状態値を取得 |
+| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | JSON シリアライズ可能な状態値を保存 |
+| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | 状態値を削除 |
+| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | 指定スコープの状態キーを一覧表示 |
+
+`scope` は `conversation`、`actor`、`subject`、`runner` のいずれかです。これらのメソッドには `ctx.context.available_apis.state` が `true` である必要があります。
+
+### 実行記録
+
+| メソッド | 戻り値 | 利用可否フィールド |
+| --- | --- | --- |
+| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` |
+| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` |
+| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` |
+| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` |
+| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` |
+| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` |
+
+`run_id=None` は現在の実行を指します。通常のイベントハンドラーは SDK が自動的に完了します。独自の `run(ctx)` が `RunnerResult` を yield する場合、同じ結果を `run_append_result()` で重複登録しないでください。
+
+### 利用可否と権限
+
+プロンプト、履歴、イベント、状態、実行記録の API が利用可能かどうかは `ctx.context.available_apis` で確認します。許可されたモデル、ツール、ナレッジベースはそれぞれ `ctx.resources.models`、`ctx.resources.tools`、`ctx.resources.knowledge_bases` にあります。
+
+Runner の実行中に `self.plugin` から呼び出すモデル、ツール、ナレッジベース、ストレージ、プラットフォーム API は、自動的に現在の実行へ関連付けられ、`ctx.resources` に基づいて再検証されます。`run_id` を渡す必要はありません。ハンドラー終了後にバックグラウンドタスクからこのコンテキストや実行に紐づく呼び出しを使用しないでください。
+
+`ctx.api` は低レベルの実行スコーププロキシです。通常のコンポーネントコードでは、このページに記載した `ctx` メソッドと、LangBot API 用の `self.plugin` を使用してください。
+
+## データ専用コンテキスト
+
+以下のコンポーネントが受け取るものはタスク入力であり、EventListener や Runner の実行コンテキストメソッドはありません。フィールド、戻り値、完全な例は各コンポーネントガイドに記載しています。
+
+| コンポーネントの入口 | 主なデータ | コンポーネントガイド |
+| --- | --- | --- |
+| `KnowledgeEngine.ingest(context: IngestionContext)` | ファイル、対象ナレッジベース、作成設定、解析結果 | [KnowledgeEngine](/ja/plugin/dev/components/knowledge-engine#ドキュメント取り込み) |
+| `KnowledgeEngine.retrieve(context: RetrievalContext)` | クエリ、対象ナレッジベース、検索設定、フィルター | [KnowledgeEngine](/ja/plugin/dev/components/knowledge-engine#ナレッジ検索) |
+| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | クエリと外部ナレッジベース設定 | [KnowledgeRetriever](/ja/plugin/dev/components/knowledge-retriever#検索コンテキスト) |
+| `Parser.parse(context: ParseContext)` | ファイル内容、ファイル名、MIME タイプ、メタデータ | [Parser](/ja/plugin/dev/components/parser#解析メソッド) |
+| `Page.handle_api(request: PageRequest)` | エンドポイント、HTTP メソッド、本文、ヘッダー | [Page](/ja/plugin/dev/components/page#pagerequest-フィールド) |
diff --git a/ja/plugin/dev/apis/common.mdx b/ja/plugin/dev/apis/common.mdx
index 7eb56e3bd..5b8284839 100644
--- a/ja/plugin/dev/apis/common.mdx
+++ b/ja/plugin/dev/apis/common.mdx
@@ -1,116 +1,15 @@
---
-title: "プラグイン共通API"
+title: "LangBot API"
---
-LangBotは、プラグインがさまざまなLangBotモジュールを操作し、メッセージコンテキストを制御するための一連のAPIを提供します。
-
-## リクエストAPI
-
-現在処理中のユーザーリクエスト(メッセージ)に対する操作です。`EventListener`と`Command`コンポーネントでのみ使用可能です。アクセス方法:
-
-- `EventListener`のイベントハンドラメソッド内: `event_context: context.EventContext`オブジェクトの内部メソッド
-- `Command`のサブコマンドハンドラメソッド内: `context: ExecuteContext`オブジェクトの内部メソッド
-
-### 直接返信メッセージ
-
-現在のリクエストが存在するセッションにメッセージチェーンで直接返信します。
-
-メッセージチェーンの構築方法については、[メッセージプラットフォームエンティティ](/en/plugin/dev/apis/messages)を参照してください。
-
-```python
-async def reply(
- self, message_chain: platform_message.MessageChain, quote_origin: bool = False
-):
- """メッセージリクエストに返信する
-
- Args:
- message_chain (platform.types.MessageChain): LangBotメッセージチェーン
- quote_origin (bool): 元のメッセージを引用するかどうか
- """
-
-# 使用例
-await event_context.reply(
- platform_message.MessageChain([
- platform_message.Plain(text="Hello, world!"),
- ]),
-)
-```
-
-### Bot UUIDの取得
-
-現在のリクエストを発行したBotのUUIDを取得します。
-
-```python
-async def get_bot_uuid(self) -> str:
- """Bot UUIDを取得する"""
-
-# 使用例
-bot_uuid = await event_context.get_bot_uuid()
-```
-
-### リクエスト変数の設定
-
-単一のリクエスト内の一部の情報は、リクエスト変数に保存されます。Difyなどの外部Agent プラットフォームを使用する場合、[これらの変数はAgent プラットフォームに明示的に渡されます](/en/deploy/pipelines/readme.html#request-variables)。
-
-```python
-async def set_query_var(self, key: str, value: Any):
- """クエリ変数を設定する"""
-
-# 使用例
-await event_context.set_query_var("key", "value")
-```
-
-### リクエスト変数の取得
-
-単一のリクエスト変数を取得します。
-
-```python
-async def get_query_var(self, key: str) -> Any:
- """クエリ変数を取得する"""
-
-# 使用例
-query_var = await event_context.get_query_var("key")
-```
-
-### すべてのリクエスト変数の取得
-
-```python
-async def get_query_vars(self) -> dict[str, Any]:
- """すべてのクエリ変数を取得する"""
-
-# 使用例
-query_vars = await event_context.get_query_vars()
-```
-
-### 現在のパイプラインに設定されたナレッジベース一覧の取得
-
-現在のリクエストが使用するパイプラインに設定されたナレッジベースの一覧を取得します。
-
-```python
-async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]:
- """List knowledge bases configured for the current pipeline"""
-
-# 使用例
-knowledge_bases = await event_context.list_pipeline_knowledge_bases()
-
-# 返り値の例
-[
- {
- "uuid": "kb_uuid",
- "name": "Product Docs",
- "description": "社内製品ドキュメント",
- }
-]
-```
-
-現在のリクエストに対応するパイプラインの `Local Agent` 設定にバインドされたナレッジベースのみを返します。現在のパイプラインにナレッジベースが設定されていない場合、または `Local Agent` を使用していない場合は、空のリストが返されます。
+モデル、ツール、ナレッジベース、ストレージには `self.plugin` を使用します。現在のイベント、返信、会話の状態については[コンテキスト API](/ja/plugin/dev/apis/agent-run) を参照してください。
## LangBot API
-これらのAPIは、任意のプラグインコンポーネントで呼び出すことができます。アクセス方法:
+これらのAPIは、通常のプラグインコンポーネントで呼び出すことができます。アクセス方法:
- プラグインルートディレクトリの`main.py`内: `self`オブジェクトの内部メソッド。これらのAPIはすべて、プラグインクラスの親クラス`BasePlugin`によって提供されます。
-- 任意のプラグインコンポーネントクラス内: `self.plugin`オブジェクトの内部メソッド。
+- 通常のプラグインコンポーネントクラス内: `self.plugin`オブジェクトの内部メソッド。
### プラグイン設定の取得
@@ -224,6 +123,7 @@ async def invoke_llm(
messages: list[provider_message.Message],
funcs: list[resource_tool.LLMTool] = [],
extra_args: dict[str, Any] = {},
+ timeout: float | None = None,
) -> provider_message.Message:
"""LLMモデルを呼び出す"""
@@ -236,6 +136,45 @@ llm_message = await self.plugin.invoke_llm(
)
```
+
+### モデルのストリーミングと使用量
+
+| メソッド | 戻り値 |
+| --- | --- |
+| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None)` | `LLMInvokeResult` |
+| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={})` | `MessageChunk` の非同期イテレーター |
+| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={})` | `LLMStreamEvent` の非同期イテレーター |
+
+
+引数は `invoke_llm` と同じです。`timeout` は秒数で、非ストリーミング呼び出しの既定値は 120 秒です。トークン使用量が必要な場合は `invoke_llm_with_usage` を使用します:
+
+```python
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+result = await self.plugin.invoke_llm_with_usage(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+)
+print(result.message.content)
+if result.usage is not None:
+ print(result.usage.total_tokens)
+```
+
+`usage` は `None` の場合があり、`prompt_tokens`、`completion_tokens`、`total_tokens` も欠ける場合があります。未取得は使用量ゼロを意味しません。キャッシュなどの追加フィールドは保持されます。
+
+```python
+async for event in self.plugin.invoke_llm_stream_events(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+):
+ if event.chunk is not None:
+ print(event.chunk.content)
+ if event.usage is not None:
+ print(event.usage.model_dump())
+```
+
+`LLMStreamEvent` は任意の `chunk` と `usage` を持ち、最後が使用量のみの場合もあります。`invoke_llm_stream` はメッセージ・ツール呼び出しのチャンクのみ返します。`MessageChunk.content` は現在の断片、`all_content` は提供される場合に累積テキストです。累積テキストは置換し、差分だけ追加します。`is_final` のチャンクにもテキストが含まれる場合があります。モデルのストリーミングはプラットフォームに送信しません。送信方法は[実行コンテキスト API](/ja/plugin/dev/apis/agent-run)を参照してください。
+
### 利用可能な Parser の一覧取得
ホスト上で現在利用可能な Parser プラグインを列挙します。MIME タイプで絞り込むこともできます。
@@ -428,7 +367,7 @@ file_bytes = await self.plugin.get_config_file("key")
## ナレッジベース API
-これらのAPIは任意のコンポーネントから`self.plugin`経由でアクセスでき、パイプライン制限なしにLangBotインスタンス内のすべてのナレッジベースの一覧取得と検索が可能です。
+これらのAPIは通常のコンポーネントから`self.plugin`経由でアクセスでき、パイプライン制限なしにLangBotインスタンス内のすべてのナレッジベースの一覧取得と検索が可能です。
### すべてのナレッジベースの一覧取得
@@ -486,7 +425,7 @@ for entry in results:
```
-これらのAPIは`query_id`を必要とせず、任意のコンポーネント(Toolコンポーネントを含む)で使用できます。現在のパイプライン設定に制限されることなく、すべてのナレッジベースにアクセスできます。
+これらのAPIは`query_id`を必要とせず、通常のコンポーネント(Toolコンポーネントを含む)で使用できます。現在のパイプライン設定に制限されることなく、すべてのナレッジベースにアクセスできます。
## RAG API
@@ -676,3 +615,95 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
# 使用例
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
```
+
+
+## その他のリソース API
+
+以下も `self.plugin` から呼び出します。
+
+### リランクモデルの呼び出し
+
+```python
+async def invoke_rerank(
+ self, rerank_model_uuid: str, query: str, documents: list[str],
+ top_k: int | None = None, extra_args: dict[str, Any] | None = None,
+ timeout: float = 60.0,
+) -> list[dict[str, Any]]:
+ ...
+
+scores = await self.plugin.invoke_rerank(
+ rerank_model_uuid=rerank_model_uuid,
+ query="How do I configure a bot?",
+ documents=["Bot configuration guide", "Plugin publishing guide"],
+ top_k=1,
+)
+```
+
+結果には通常、元の文書の `index` と関連度 `relevance_score` が含まれます。
+
+### パーサーの呼び出し
+
+```python
+async def invoke_parser(
+ self, plugin_author: str, plugin_name: str, storage_path: str,
+ mime_type: str, filename: str, metadata: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ ...
+
+parsers = await self.plugin.list_parsers(mime_type="application/pdf")
+if parsers:
+ parser = parsers[0]
+ parsed = await self.plugin.invoke_parser(
+ plugin_author=parser["plugin_author"],
+ plugin_name=parser["plugin_name"],
+ storage_path=storage_path,
+ mime_type="application/pdf",
+ filename="guide.pdf",
+ )
+```
+
+`storage_path` はナレッジ取り込み時の `context.file_object.storage_path` など、ホストの保存先です。プラグインのローカルパスではありません。結果の辞書には `text`、`sections`、`metadata` が含まれます。
+
+### ベクトルレコード一覧
+
+```python
+async def vector_list(
+ self, collection_id: str, filters: dict[str, Any] | None = None,
+ limit: int = 20, offset: int = 0,
+) -> dict[str, Any]:
+ ...
+
+page = await self.plugin.vector_list(
+ collection_id=collection_id,
+ filters={"file_id": "doc_001"},
+ limit=20,
+ offset=0,
+)
+for item in page["items"]:
+ print(item["id"], item.get("metadata"))
+```
+
+`items` と `total` を返します。各項目は `id`、`document`、`metadata` を含み、`total` はストレージが取得可能な一致件数です。フィルターは上記のベクトル API と同じ形式です。
+
+### プラグインとコマンド一覧
+
+```python
+manifests = await self.plugin.list_plugins_manifest()
+commands = await self.plugin.list_commands()
+```
+
+いずれも引数なしで、ホストが提供するプラグインのマニフェスト一覧とコマンド一覧を返します。
+
+### Box サンドボックス
+
+`self.plugin` から現在のワークスペースに対して呼び出します。Runner 内では実行権限が自動的に引き継がれます。
+
+| メソッド | 戻り値 | 説明 |
+| --- | --- | --- |
+| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`、`available`、上限 `limit`、使用数 `used`、残数 `remaining`、任意の `required_reuse_key`、利用不可理由 `reason` |
+| `await self.plugin.list_boxes()` | `list[BoxSession]` | `id` と状態 `status`(`idle`、`running`、`closing`)のスナップショット |
+| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | 同一キーで再利用、または上限内で作成。`options` は `image` をサポート |
+
+不明な数量は `None` です。残数がゼロでも既存 Box の再利用は可能です。新規作成時の上限は Runtime が原子的に確認します。キーは Runner が計算しますが、`required_reuse_key` が返された場合はその値を使います。異なるワークスペース間では同じキーでも共有されません。
+
+取得後は[Runner コンテキスト](/ja/plugin/dev/apis/agent-run#runner)でバインドしてから、添付のインポートやツール実行を行います。
diff --git a/ja/plugin/dev/apis/messages.mdx b/ja/plugin/dev/apis/messages.mdx
index 130ca772d..90d43f35e 100644
--- a/ja/plugin/dev/apis/messages.mdx
+++ b/ja/plugin/dev/apis/messages.mdx
@@ -58,34 +58,8 @@ msg_chain = MessageChain([
## その他のエンティティ
-上記のエンティティに加えて、`Friend`や`Group`などのエンティティもあります。これらのエンティティは`langbot_plugin.api.entities.platform.entities`で定義されています。
+上記のエンティティに加えて、`Friend`や`Group`などのエンティティもあります。これらのエンティティは`langbot_plugin.api.entities.builtin.platform.entities`で定義されています。
-## 基盤プラットフォームAPIへのアクセス
+## プラットフォーム API
-
- 4.xには適用されません。
-
-
-プラットフォームの違いを抽象化するために、LangBotはメッセージングプラットフォームAPIの上に抽象化レイヤーを提供しています。これには、上記で説明した内容と、`pkg/platform/sources`ディレクトリ内のさまざまなプラットフォームアダプターが含まれます。 ただし、プラットフォームの違いが大きいため、抽象化レイヤーのエンティティとAPIのカプセル化は完全ではありません。プラグインがプラットフォーム固有の機能にアクセスする必要がある場合は、次の手順に従って基盤APIにアクセスできます。
-
-
- ```python
- # 例えばプラグイン内で
- @handler(GroupMessageReceived)
- async def _(self, ctx: EventContext):
- # アダプターオブジェクトを取得
- adapter = ctx.query.adapter
- ```
-
-
-| プラットフォーム | アダプター | アクセス方法 | 説明 |
-| ------------- | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| OneBot v11 | aiocqhttp | adapter.bot | botオブジェクトは[aiocqhttp](https://github.com/nonebot/aiocqhttp) CQHttpオブジェクトに対応します。詳細な使用方法についてはaiocqhttpドキュメントを参照してください |
-| QQ公式API | qqofficial | adapter.bot | botオブジェクトはlibs/qq_official_api以下のSDKオブジェクトです。詳細な使用方法については、libs/qq_official_api/api.pyの`QQOfficialClient`内の`send_group_text_msg`メソッドを参照してHTTPリクエストメソッドを確認し、具体的なインターフェースドキュメントについてはQQ公式APIドキュメントを参照してください |
-| WeCom(企業微信) | wecom | adapter.bot | libs/wecom_api/api.pyの`WecomClient`内の`send_image`メソッドを参照してHTTPリクエストメソッドを確認し、具体的なインターフェースドキュメントについてはWeCom APIドキュメントを参照してください |
-| WeChat公式アカウント | officialaccount | - | インターフェースドキュメントについてはWeChat公式アカウントAPIドキュメントを参照してください。認証情報はadapter.botから取得できます |
-| Lark | lark | adapter.api_client | Lark SDKのAPI Clientオブジェクトです。詳細については[oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python)を参照してください |
-| DingTalk | dingtalk | - | DingTalk APIドキュメントを参照してください。認証情報はadapter.botから取得できます |
-| Discord | discord | adapter.bot | botオブジェクトは[Rapptz/discord.py](https://github.com/Rapptz/discord.py) Discordオブジェクトです。詳細な使用方法についてはドキュメントを参照してください |
-| Telegram | telegram | adapter.bot | botオブジェクトは[python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) Telegramオブジェクトです。詳細な使用方法についてはドキュメントを参照してください |
-| Slack | slack | adapter.bot | botオブジェクトはlibs/slack_api/api.py内にあります。詳細な使用方法についてはそのコードとSlack SDKを参照してください |
\ No newline at end of file
+ユーザー取得、グループ管理、固有の操作には[プラットフォーム API](/ja/plugin/dev/apis/platform)を使用してください。Runner も同じ `self.plugin` を使用し、現在の会話には `ctx.reply()` で返信します。
diff --git a/ja/plugin/dev/apis/pipeline-events.mdx b/ja/plugin/dev/apis/pipeline-events.mdx
index d7bd79052..97554f7b5 100644
--- a/ja/plugin/dev/apis/pipeline-events.mdx
+++ b/ja/plugin/dev/apis/pipeline-events.mdx
@@ -248,42 +248,13 @@ class PromptPreProcessing(BaseEventModel):
"""この会話の既存メッセージレコード、変更可能、langbot_plugin.api.entities.builtin.provider.message.Messageまたはlangbot_plugin.api.entities.builtin.provider.message.MessageChunk型"""
```
-## イベントコンテキストAPI
+## EventListener コンテキスト
-```python
-...
- @self.handler(events.PersonMessageReceived)
- async def handler(event_context: context.EventContext):
- ...
-```
+イベントハンドラーは `event_context.event` から現在のイベントを取得します。`EventContext` の完全なプロパティとメソッドは[コンテキスト API](/ja/plugin/dev/apis/agent-run#eventlistener)を参照してください。
-イベント処理メソッドには`EventContext`オブジェクトが渡され、イベントコンテキスト情報が含まれます。このオブジェクトには[リクエストAPI](/ja/plugin/dev/apis/common)とイベントコンテキスト固有のAPIの両方があります。以下は、イベントコンテキスト固有のAPIのリストです:
+### デフォルト動作を阻止
-### 元のイベント属性の取得
-
-`event_context.event`属性は元のイベントオブジェクトであり、元のイベントの属性を取得できます。型はリッスンされているイベントの型です。
-
-例えば、リッスンされているイベントが`GroupMessageReceived`の場合:
-
-```python
-event = event_context.event
-print(event.launcher_type) # 起動元オブジェクトタイプ(group)
-print(event.launcher_id) # グループID
-print(event.sender_id) # 送信者アカウントID
-print(event.message_chain) # メッセージチェーン
-```
-
-### デフォルト動作を防止
-
-```python
-def prevent_default(self):
- """デフォルト動作を防止する"""
-
-# 使用例
-event_context.prevent_default()
-```
-
-このメソッドを呼び出すと、このイベントのデフォルトの後続動作が防止され、パイプラインは直接終了します。
+`event_context.prevent_default()` を呼び出すと、現在のイベントのデフォルト処理が停止します。この操作を利用できるイベントは次のとおりです。
以下のイベントのみデフォルト動作を阻止できます:
@@ -297,14 +268,4 @@ event_context.prevent_default()
- NormalMessageResponded
-### 後続実行を防止
-
-```python
-def prevent_postorder(self):
- """後続実行を防止する"""
-
-# 使用例
-event_context.prevent_postorder()
-```
-
-このメソッドを呼び出すと、後続のプラグインがこの時点で実行されなくなります。
\ No newline at end of file
+`event_context.prevent_postorder()` を呼び出すと、後続プラグインによる現在のイベント処理を停止できます。
diff --git a/ja/plugin/dev/apis/platform.mdx b/ja/plugin/dev/apis/platform.mdx
new file mode 100644
index 000000000..6ba881862
--- /dev/null
+++ b/ja/plugin/dev/apis/platform.mdx
@@ -0,0 +1,78 @@
+---
+title: "プラットフォーム API"
+---
+
+ボット UUID を指定してプラットフォームアダプターの API を呼び出します。メッセージチェーンの送信は `self.plugin.send_message()`、グループやユーザーの取得などは以下を使用します。
+
+## プラットフォーム API の呼び出し
+
+```python
+async def call_platform_api(
+ self,
+ bot_uuid: str,
+ action: str,
+ params: dict[str, Any] | None = None,
+) -> Any:
+ ...
+```
+
+Command、EventListener、Tool などのコンポーネントでは `self.plugin` から呼び出します。戻り値は操作に応じた辞書、リスト、スカラー、`None` で、プラットフォーム SDK のオブジェクトではありません。
+
+```python
+group = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="get_group_info",
+ params={"group_id": "123456"},
+)
+```
+
+`bot_uuid` は `await self.plugin.get_bots()`、またはパイプラインイベントの `await event_context.get_bot_uuid()` で取得できます。プラットフォーム ID には実際のイベントや設定の値を使用します。
+
+### 主な操作
+
+| `action` | `params` |
+| --- | --- |
+| `get_user_info` | `user_id` |
+| `get_friend_list` | `{}` |
+| `get_group_info` | `group_id` |
+| `get_group_list` | `{}` |
+| `get_group_member_list` | `group_id` |
+| `get_group_member_info` | `group_id`, `user_id` |
+| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` |
+| `set_group_name` | `group_id`, `name` |
+| `mute_member` | `group_id`, `user_id`, `duration` |
+| `unmute_member` / `kick_member` | `group_id`, `user_id` |
+| `leave_group` | `group_id` |
+| `approve_friend_request` | `request_id`, `approve`, `remark` |
+| `approve_group_invite` | `request_id`, `approve` |
+| `get_file_url` | `file_id` |
+
+
+対応する操作はプラットフォームごとに異なり、ボットの管理者権限が必要な場合があります。`duration` は秒数で、`0` の意味はプラットフォームに依存します。共通の永久ミュート値ではありません。取得結果がキャッシュに依存することもあります。ボットが未起動、未対応の操作、引数不正の場合は例外になります。
+
+## プラットフォーム固有の API
+
+外側の `action` を `call_platform_api` にし、`params` にプラットフォームの操作と引数を指定します。OneBot Omni でボットのアカウント情報を取得する例です:
+
+```python
+account = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="call_platform_api",
+ params={"action": "get_login_info", "params": {}},
+)
+```
+
+アダプターが宣言した操作のみ呼び出せます。すべてのプラットフォーム API を任意に転送できるわけではありません。プラグインはランタイム経由でホストを呼び出し、`ctx.query.adapter` や `adapter.bot` には直接アクセスしません。
+
+## Runner
+
+Runner の実行中、`self.plugin` によるモデル、ツール、ナレッジベース、ストレージ、プラットフォーム呼び出しは現在の実行と権限に自動的に関連付けられます。`run_id` の指定は不要です。同時実行は互いに分離されます。ハンドラー終了後に実行に紐づく API を呼び続けないでください。
+
+現在のボットは `await ctx.get_bot_uuid()` で取得します。プラットフォーム呼び出しは今回のツール権限に従い、イベント対象だけが許可されている場合は別の対象を指定できません。プラットフォーム固有の操作を含む未許可の操作は拒否されます。デバッグではこれらの呼び出しも Mock を使用します。
+
+```python
+tools = await ctx.get_available_tools()
+if any(tool["name"] == "event_get_actor" for tool in tools):
+ actor = await ctx.call_tool("event_get_actor")
+ await ctx.log(str(actor))
+```
diff --git a/ja/plugin/dev/basic-info.mdx b/ja/plugin/dev/basic-info.mdx
index 00426dfdc..6c9b42a98 100644
--- a/ja/plugin/dev/basic-info.mdx
+++ b/ja/plugin/dev/basic-info.mdx
@@ -159,6 +159,7 @@ spec:
```yaml
- name: mode
type: select
+ allow_custom: true
options: # ドロップダウンオプション、多言語サポート。
- name: mode1 # 値、ドロップダウンで小さなテキストとして表示。
label: # 表示名、多言語サポート。
@@ -171,6 +172,8 @@ spec:
...
```
+`allow_custom: true` を設定すると、プリセットに「カスタム」が追加され、その選択時だけ入力欄が表示されます。保存される値は選択した `name` または入力した文字列であり、「カスタム」というマーカーではありません。ランナーの `spec.config` にも使用できます。
+
### type: prompt-editor
プロンプトエディター。パイプライン設定ページからプロンプトエディターを表示し、最終結果はOpenAIの`messages`形式で表されます。
diff --git a/ja/plugin/dev/components/add.mdx b/ja/plugin/dev/components/add.mdx
index 48a5902b7..8ca1425c1 100644
--- a/ja/plugin/dev/components/add.mdx
+++ b/ja/plugin/dev/components/add.mdx
@@ -4,6 +4,8 @@ title: "コンポーネントの追加"
プラグインは、LangBotにさまざまな機能を提供する1つ以上のコンポーネントで構成されています。現在サポートされているコンポーネントタイプには次のものがあります:
+- プラグインプロセッサー(Runner):インスタンスを作成してイベントに紐付け、プラグインのコードで処理します。[チュートリアル](/ja/plugin/dev/components/runner)を参照してください。
+- Agent ランナー(Runner):Agent の設定で選択する実行コンポーネントです。独立したプラグインプロセッサーとは異なります。
- イベントリスナー(EventListener): パイプライン実行中のイベントをリッスンし、コンテキストやパイプラインを変更します。
- コマンド(Command): `!`(または他の設定されたプレフィックス)で始まるユーザーコマンドメッセージによってトリガーされます。
- ツール(Tool): LangBotの組み込みLocal Agentの実行中にLLMによって呼び出されます。
@@ -70,6 +72,7 @@ components:
各コンポーネントの詳細な使用方法については、次を参照してください:
+- [Runner](./runner)
- [EventListener](./event-listener)
- [Command](./command)
- [Tool](./tool)
diff --git a/ja/plugin/dev/components/command.mdx b/ja/plugin/dev/components/command.mdx
index bb6b0dc7d..db9523a2e 100644
--- a/ja/plugin/dev/components/command.mdx
+++ b/ja/plugin/dev/components/command.mdx
@@ -211,4 +211,5 @@ yield CommandReturn(
コマンド登録とコマンド実行の基本情報を学びました。次は:
-- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください
+- 完全な [`ExecuteContext` API リファレンス](/ja/plugin/dev/apis/agent-run#command)を確認します。
+- [LangBot API](/ja/plugin/dev/apis/common)を確認します。
diff --git a/ja/plugin/dev/components/event-listener.mdx b/ja/plugin/dev/components/event-listener.mdx
index e14f12e54..e1cd24f8b 100644
--- a/ja/plugin/dev/components/event-listener.mdx
+++ b/ja/plugin/dev/components/event-listener.mdx
@@ -2,6 +2,10 @@
title: "コンポーネント: イベントリスナー"
---
+
+このコンポーネントはパイプラインの処理を拡張します。メッセージ受信やメンバー参加などの Bot イベントを処理する場合は、[イベントプロセッサー](/ja/plugin/dev/components/runner)を参照してください。
+
+
LangBotのパイプラインは実行時にイベントを生成し、プラグインがフックして処理できるようにします。各プラグインは1つのイベントリスナー(components/event_listener/default.yaml)のみを持つことができますが、その中で任意の数のイベントを登録できます。
## イベントリスナーコンポーネントの追加
@@ -99,5 +103,6 @@ class EventContext(pydantic.BaseModel):
イベントリスナーの基本情報を学びました。次は:
-- ドキュメント[パイプラインイベント](/en/plugin/dev/apis/pipeline-events)で`監視可能なイベントのリスト`と`イベントコンテキストAPI`を確認してください
-- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください
+- [パイプラインイベント](/ja/plugin/dev/apis/pipeline-events)でイベントの種類とフィールドを確認します。
+- 完全な [`EventContext` API リファレンス](/ja/plugin/dev/apis/agent-run#eventlistener)を確認します。
+- [LangBot API](/ja/plugin/dev/apis/common)を確認します。
diff --git a/ja/plugin/dev/components/runner.mdx b/ja/plugin/dev/components/runner.mdx
new file mode 100644
index 000000000..466365010
--- /dev/null
+++ b/ja/plugin/dev/components/runner.mdx
@@ -0,0 +1,199 @@
+---
+title: "コンポーネント: Runner"
+---
+
+Runner は Agent の実行ロジック、または「プラグインプロセッサー」用のイベント処理関数を実装するコンポーネントです。どちらも `RunnerContext` を受け取り、インストール後にユーザーがコンポーネントを選択・設定します。
+
+環境の準備は[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)を参照してください。
+
+`RunnerContext` の完全なフィールドとメソッドは[コンテキスト API](/ja/plugin/dev/apis/agent-run#runner)を参照してください。
+
+## Runner コンポーネントの追加
+
+1 つのプラグインに複数のイベントプロセッサーを追加できます。プラグインのディレクトリで `lbp comp Runner` を実行し、名前に `welcome`、説明に `Welcome new members` を入力します。
+
+```bash
+lbp comp Runner
+```
+
+生成される `components/runner/welcome.yaml` はコンポーネントを定義するファイルで、`welcome.py` に処理を記述します。
+
+## 実行ロジックの実装
+
+モデルの接続や独自の実行ループには `spec.usages: [agent]` を宣言し、`run(ctx)` から結果を返します:
+
+```python
+from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+
+class Echo(Runner):
+ async def run(self, ctx: RunnerContext):
+ yield RunnerResult.run_completed(
+ ctx.run_id,
+ message=Message(role="assistant", content=ctx.input.to_text()),
+ )
+```
+
+## マニフェストファイル: Runner
+
+`welcome.yaml` を次の内容に変更します。
+
+```yaml
+apiVersion: langbot/v1
+kind: Runner
+metadata:
+ name: welcome
+ label:
+ en_US: Welcome members
+ zh_Hans: 欢迎新成员
+ ja_JP: 新しいメンバーを歓迎
+ description:
+ en_US: Welcome new members and respond to /hello.
+ zh_Hans: 欢迎新成员,并响应 /hello 消息。
+ ja_JP: 新しいメンバーを歓迎し、/hello に返信します。
+spec:
+ usages: [event]
+ events:
+ - group.member_joined
+ - message.received
+ capabilities:
+ tool_calling: true
+ permissions:
+ tools: [detail, call]
+ config:
+ - name: greeting
+ type: string
+ label:
+ en_US: Greeting
+ zh_Hans: 欢迎语
+ ja_JP: 歓迎メッセージ
+ required: true
+ default: Welcome aboard!
+ - name: reply_enabled
+ type: boolean
+ label:
+ en_US: Send replies
+ zh_Hans: 发送回复
+ ja_JP: 返信する
+ default: true
+execution:
+ python:
+ path: ./welcome.py
+ attr: Welcome
+```
+
+`spec.events` は対応するイベント、`spec.config` は詳細画面でユーザーが変更できる設定です。この例ではメンバー参加とメッセージ受信に対応し、歓迎メッセージと返信の有効・無効を設定できます。返信には、上記の `tool_calling` と `permissions.tools` の指定が必要です。
+
+
+`spec.usages` は必須で、デフォルト値はありません。`spec.usages` は選択先を指定します。`agent` は Agent とパイプライン、`event` はプラグインプロセッサーです。`[agent, event]` の併記も可能です。`event` には `spec.events` が必要です。以下はイベント処理関数の例です。独自の `run(ctx)` から `await super().run(ctx)` で呼び出すこともできます。
+
+## プラグインの実装
+
+`Welcome` クラスの `initialize` メソッド内で処理関数を登録します。`welcome.py` を次の内容に変更してください。
+
+```python
+from langbot_plugin.api.definition.components.runner import (
+ Runner,
+ RunnerContext,
+)
+from langbot_plugin.api.entities.builtin.platform.events import (
+ MemberJoinedEvent,
+ MessageReceivedEvent,
+)
+from langbot_plugin.api.entities.builtin.platform.message import Plain
+
+
+class Welcome(Runner):
+ async def initialize(self):
+ await super().initialize()
+
+ @self.handler(MemberJoinedEvent)
+ async def on_join(ctx: RunnerContext):
+ name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id)
+ await ctx.log(f"Member joined: {name}")
+ if not ctx.config.get("reply_enabled", True):
+ await ctx.log("Replies are disabled for this processor")
+ return
+ greeting = ctx.config.get("greeting", "Welcome aboard!")
+ await ctx.reply(f"{name}, {greeting}")
+ await ctx.log("Welcome action completed")
+
+ @self.handler(MessageReceivedEvent)
+ async def on_message(ctx: RunnerContext):
+ text = "".join(
+ item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain)
+ ).strip()
+ if text != "/hello":
+ await ctx.log("Message ignored: expected /hello")
+ return
+ if ctx.config.get("reply_enabled", True):
+ await ctx.reply(ctx.config.get("greeting", "Welcome aboard!"))
+ else:
+ await ctx.log("Replies are disabled for this processor")
+```
+
+`on_join` は参加したメンバーに挨拶し、`on_message` は `/hello` に返信します。「返信する」を無効にすると、ログだけを記録します。
+
+`ctx.platform_event` は現在のイベント、`ctx.config` はこのプロセッサーの設定です。返信には `ctx.reply()`、ログの記録には `ctx.log()` を使います。関数の実行が終わると、イベントの処理も終了します。
+
+## ストリーミング返信
+
+`ctx.reply_stream()` で同じ返信を更新します。`update(text)` には、その時点の全文を渡します。
+
+```python
+async with ctx.reply_stream() as reply:
+ await reply.update("処理中…")
+ await reply.update("完了しました。")
+```
+
+`async with` ブロックが正常終了すると返信が完了します。ストリーミングが未対応・無効の場合やメッセージ以外のイベントでは、最後に全文を一度だけ送信します。例外時には、バッファ内の未完成テキストを送信しません。デバッグでは送信をシミュレーションします。
+
+
+## イベントの登録
+
+`@self.handler(イベントクラス)` で処理関数を登録し、対応する識別子を `spec.events` に追加します。例えば `MemberJoinedEvent` は `group.member_joined` に対応します。
+
+| イベント | マニフェストの識別子 | SDK のイベントクラス |
+| --- | --- | --- |
+| メッセージ受信 | `message.received` | `MessageReceivedEvent` |
+| メッセージ編集 | `message.edited` | `MessageEditedEvent` |
+| メッセージ削除 | `message.deleted` | `MessageDeletedEvent` |
+| メッセージへのリアクション | `message.reaction` | `MessageReactionEvent` |
+| フィードバック受信 | `feedback.received` | `FeedbackReceivedEvent` |
+| メンバー参加 | `group.member_joined` | `MemberJoinedEvent` |
+| メンバー退出 | `group.member_left` | `MemberLeftEvent` |
+| メンバーのミュート | `group.member_banned` | `MemberBannedEvent` |
+| グループ情報更新 | `group.info_updated` | `GroupInfoUpdatedEvent` |
+| 友達申請受信 | `friend.request_received` | `FriendRequestReceivedEvent` |
+| 友達追加 | `friend.added` | `FriendAddedEvent` |
+| 友達削除 | `friend.removed` | `FriendRemovedEvent` |
+| Bot のグループ招待 | `bot.invited_to_group` | `BotInvitedToGroupEvent` |
+| Bot のグループからの削除 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` |
+| Bot のミュート | `bot.muted` | `BotMutedEvent` |
+| Bot のミュート解除 | `bot.unmuted` | `BotUnmutedEvent` |
+| プラットフォーム固有のイベント | `platform.specific` | `PlatformSpecificEvent` |
+
+利用できるイベントとフィールドは、SDK の [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)を参照してください。対応イベントはプラットフォームによって異なります。
+
+## イベントプロセッサーのテスト
+
+[プラグイン開発チュートリアル](/ja/plugin/dev/tutor)に従ってデバッグ接続を設定し、プラグインのディレクトリで `lbp run` を実行します。LangBot で次の操作を行ってください。
+
+1. 「プラグインプロセッサー」を作成し、詳細画面で「新しいメンバーを歓迎」を選び、設定を保存します。
+2. 左側でメンバー参加を選択し、ニックネーム、メンバー ID、グループ ID を入力してテストを実行します。
+3. 返信とログを確認します。メッセージ受信を選んで `/hello` を入力すると、返信のテストもできます。
+
+デバッグ中の返信は Mock で再現され、実際のメッセージは送信されません。コンポーネントの YAML を変更したら `lbp run` を再起動してください。
+
+インストール後、ボット詳細の「プラグインプロセッサー」でこの設定を追加して保存します。この画面でコンポーネントを選択し、設定を入力して新規作成・紐付けすることもできます。宣言されたイベントは自動で配信され、イベントごとのルート設定は不要です。
+
+プラグインの購読は Agent/パイプラインのルートと独立して実行されます。同じイベントを複数のプロセッサーが処理でき、一つが失敗しても他の実行を妨げません。同じ設定を使用するボットは設定内容と実行状態を共有します。個別に設定する場合は、新しい設定を作成してください。返信などの任意の動作はコンポーネントの設定として提供し、重複返信を避けてください。
+
+## 次のステップ
+
+- [`RunnerContext` API リファレンス](/ja/plugin/dev/apis/agent-run#runner)を確認します。
+- モデル、ツール、ナレッジベース、ストレージについては [LangBot API](/ja/plugin/dev/apis/common)を確認します。
+- メッセージ送信などについては[プラットフォーム API](/ja/plugin/dev/apis/platform)を確認します。
+- [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo)で他の実装例を確認できます。
+- パイプラインの前処理やモデル呼び出し完了時などに機能を追加する場合は、[イベントリスナー](/ja/plugin/dev/components/event-listener)を使用してください。
diff --git a/ja/plugin/dev/components/tool.mdx b/ja/plugin/dev/components/tool.mdx
index 1d8825a27..8a9ab8c77 100644
--- a/ja/plugin/dev/components/tool.mdx
+++ b/ja/plugin/dev/components/tool.mdx
@@ -173,4 +173,5 @@ httpx
ツールの基本的な使い方を学びました。次は:
-- [プラグイン共通API](/en/plugin/dev/apis/common)を確認してください
+- Tool が受け取る [`session` と `query_id`](/ja/plugin/dev/apis/agent-run#tool)を確認します。
+- [LangBot API](/ja/plugin/dev/apis/common)を確認します。
diff --git a/ja/plugin/dev/tutor.mdx b/ja/plugin/dev/tutor.mdx
index 66b334a3a..1f521fcf0 100644
--- a/ja/plugin/dev/tutor.mdx
+++ b/ja/plugin/dev/tutor.mdx
@@ -2,6 +2,10 @@
title: "プラグイン開発チュートリアル"
---
+
+メッセージ受信やメンバー参加などを処理するプラグインを作る場合は、基本環境を準備した後、[イベントプロセッサーのチュートリアル](/ja/plugin/dev/components/runner)に進んでください。
+
+
## アーキテクチャの基礎
バージョン4.0では、高セキュリティで柔軟性の高い本番環境グレードのプラグインシステムを導入し、開発者に豊富なAPIと使いやすいサポートツールを提供しています。
diff --git a/scripts/README-runner-validation.md b/scripts/README-runner-validation.md
new file mode 100644
index 000000000..06d9b0d0d
--- /dev/null
+++ b/scripts/README-runner-validation.md
@@ -0,0 +1,14 @@
+# Runner tutorial verification
+
+With the local SDK installed, run:
+
+```bash
+python scripts/verify-runner-tutorial.py
+python scripts/verify-plugin-api-examples.py
+npm test
+npm run build
+```
+
+The tutorial checker extracts the published Chinese, English and Japanese examples, checks that their code and manifests match, and executes event handlers including disabled replies, empty input, private/group messages and delivery failure. Platform actions use Mock; these tests do not prove external message delivery.
+
+The official `langbot-plugin-demo/RunnerDemo` plugin adds multi-step actions, configuration isolation and progress/error examples. Its `scripts/smoke.py` installs a built package and runs the scenarios against a local Host. Use the same SDK and Core revision when testing.
diff --git a/scripts/verify-plugin-api-examples.py b/scripts/verify-plugin-api-examples.py
new file mode 100644
index 000000000..1f984550a
--- /dev/null
+++ b/scripts/verify-plugin-api-examples.py
@@ -0,0 +1,300 @@
+"""Run the API documentation snippets with the installed SDK and a mock Host.
+
+Usage: python scripts/verify-plugin-api-examples.py
+This checks SDK calls and result shapes, not external platform delivery.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import ast
+import inspect
+import re
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any
+from unittest.mock import AsyncMock
+
+from langbot_plugin.api.entities.builtin.runner.context import RunnerContext
+from langbot_plugin.api.entities.builtin.runner.errors import (
+ AgentAPIError,
+ AgentAPIException,
+)
+from langbot_plugin.api.proxies.runner import RunnerAPIProxy
+from langbot_plugin.api.proxies.langbot_api import LangBotAPIProxy
+from langbot_plugin.api.proxies.event_context import EventContextProxy
+from langbot_plugin.api.proxies.execute_context import ExecuteContextProxy
+from langbot_plugin.api.entities.builtin.provider.session import Session
+from langbot_plugin.api.proxies.invocation import bind_invocation
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class Host:
+ def __init__(self):
+ self.calls = []
+
+ async def call_action(self, action, data, timeout=15):
+ name = action.value
+ self.calls.append((name, data))
+ responses = {
+ "invoke_llm": {
+ "message": {"role": "assistant", "content": "Hello!"},
+ "usage": {"total_tokens": 8},
+ },
+ "invoke_rerank": {"results": [{"index": 0, "relevance_score": 0.9}]},
+ "list_parsers": {
+ "parsers": [{"plugin_author": "demo", "plugin_name": "Parser"}]
+ },
+ "invoke_parser": {"text": "guide", "sections": [], "metadata": {}},
+ "vector_list": {
+ "items": [{"id": "chunk", "document": "guide", "metadata": {}}],
+ "total": 1,
+ },
+ "list_plugins_manifest": {"plugins": []},
+ "list_commands": {"commands": []},
+ "call_platform_api": {"result": {"id": "123456", "name": "Example"}},
+ "call_tool": {"result": {"id": "alice"}},
+ "reply_stream": {"result": {"ok": True, "mock": True}},
+ "history_page": {
+ "items": [],
+ "has_more": not data.get("before_cursor"),
+ "prev_cursor": "older",
+ "next_cursor": None,
+ },
+ "reply_message": {},
+ "set_query_var": {},
+ "state_set": {"success": True},
+ "state_get": {"value": "setup"},
+ }
+ if name not in responses:
+ raise AssertionError(f"Unexpected Host action: {name}")
+ return responses[name]
+
+ async def call_action_generator(self, action, data, timeout=15):
+ assert action.value == "invoke_llm_stream"
+ yield {"chunk": {"role": "assistant", "content": "Hello", "is_final": False}}
+ yield {"chunk": {"role": "assistant", "content": "!", "is_final": True}}
+ yield {"usage": {"total_tokens": 8}}
+
+
+def context():
+ return RunnerContext.model_validate(
+ {
+ "run_id": "docs-run",
+ "trigger": {"type": "user_message"},
+ "event": {
+ "event_id": "docs-event",
+ "event_type": "message.received",
+ "source": "test",
+ },
+ "input": {"content": "Hello"},
+ "delivery": {"surface": "test"},
+ "runtime": {},
+ "resources": {
+ "models": [{"model_id": "model"}],
+ "tools": [
+ {"tool_name": "event_get_actor"},
+ {"tool_name": "event_reply"},
+ ],
+ },
+ "context": {
+ "available_apis": {
+ "history_page": True,
+ "state": True,
+ "reply_stream": True,
+ }
+ },
+ }
+ )
+
+
+def snippets(locale, page):
+ source = (ROOT / locale / f"plugin/dev/apis/{page}.mdx").read_text()
+ if page == "common":
+ headings = {
+ "zh": ("### 模型流式输出与用量", "## 其他资源 API"),
+ "en": ("### Model streaming and usage", "## Additional resource APIs"),
+ "ja": ("### モデルのストリーミングと使用量", "## その他のリソース API"),
+ }[locale]
+ start = source.index(headings[0])
+ end = source.index("\n### ", start + len(headings[0]))
+ source = source[start:end] + source[source.index(headings[1]) :]
+ return re.findall(r"^```python\n(.*?)^```", source, re.M | re.S)
+
+
+def verify_context_reference():
+ source = (ROOT / "zh/plugin/dev/apis/agent-run.mdx").read_text()
+ query_methods = {
+ "reply",
+ "get_bot_uuid",
+ "set_query_var",
+ "get_query_var",
+ "get_query_vars",
+ "create_new_conversation",
+ "list_pipeline_knowledge_bases",
+ "retrieve_knowledge",
+ }
+ runner_methods = {
+ name
+ for name, value in RunnerContext.__dict__.items()
+ if callable(value) and not name.startswith("_")
+ }
+ runner_methods.discard("model_post_init")
+
+ for prefix in ("event_context", "context"):
+ missing = sorted(
+ name for name in query_methods if f"{prefix}.{name}(" not in source
+ )
+ assert not missing, f"{prefix} methods missing from context reference: {missing}"
+
+ for name in ("prevent_default", "prevent_postorder"):
+ assert f"event_context.{name}(" in source
+
+ missing = sorted(name for name in runner_methods if f"ctx.{name}(" not in source)
+ assert not missing, f"RunnerContext methods missing from context reference: {missing}"
+ assert "`platform_event`" in source
+
+ required_fields = {
+ "instance_uuid",
+ "workspace_uuid",
+ "placement_generation",
+ "query_id",
+ "query_uuid",
+ "eid",
+ "event_name",
+ "event",
+ "is_prevent_default",
+ "is_prevent_postorder",
+ "session",
+ "command_text",
+ "full_command_text",
+ "command",
+ "crt_command",
+ "params",
+ "crt_params",
+ "privilege",
+ *RunnerContext.model_fields,
+ }
+ missing = sorted(name for name in required_fields if f"`{name}`" not in source)
+ assert not missing, f"Context fields missing from context reference: {missing}"
+ assert "spec.permissions" not in source
+ assert "spec.capabilities" not in source
+
+
+async def execute(block, namespace):
+ # Examples are function bodies; signature reference blocks may include stubs.
+ function = "async def example():\n" + "\n".join(
+ " " + line for line in block.splitlines()
+ )
+ tree = ast.parse(function)
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
+ continue
+ owner = ast.unparse(node.func.value)
+ cls = (
+ LangBotAPIProxy
+ if owner == "self.plugin"
+ else EventContextProxy
+ if owner == "event_context"
+ else ExecuteContextProxy
+ if owner == "context"
+ else RunnerContext
+ if owner == "ctx"
+ else RunnerAPIProxy
+ if owner == "api"
+ else None
+ )
+ if cls is not None:
+ signature = inspect.signature(getattr(cls, node.func.attr))
+ signature.bind(
+ None,
+ *([None] * len(node.args)),
+ **{kw.arg: None for kw in node.keywords},
+ )
+ exec(compile(tree, "plugin-api-doc-example", "exec"), namespace)
+ await namespace["example"]()
+
+
+async def main():
+ verify_context_reference()
+ total = 0
+ for page in ("common", "agent-run", "platform"):
+ blocks = snippets("zh", page)
+ for locale in ("en", "ja"):
+ assert blocks == snippets(locale, page), f"{page}: {locale} code differs"
+ for block in blocks:
+ host = Host()
+ ctx = context()
+ api = RunnerAPIProxy(ctx, host)
+ ctx._api = api
+ ctx._results = asyncio.Queue()
+ session = Session(launcher_type="group", launcher_id="docs-group", sender_id="docs-user")
+ namespace = {
+ "event_context": EventContextProxy.model_construct(
+ query_id=42,
+ event=SimpleNamespace(sender_id="docs-user"),
+ plugin_runtime_handler=host,
+ ),
+ "context": ExecuteContextProxy.model_construct(
+ query_id=43,
+ plugin_runtime_handler=host,
+ crt_params=["Hello!"],
+ session=session,
+ ),
+ "session": session,
+ "Any": Any,
+ "Message": Message,
+ "api": api,
+ "ctx": ctx,
+ "self": SimpleNamespace(
+ plugin=LangBotAPIProxy(host), get_run_api=lambda _: api
+ ),
+ "model_uuid": "model",
+ "rerank_model_uuid": "model",
+ "collection_id": "collection",
+ "storage_path": "files/guide.pdf",
+ "bot_uuid": "bot",
+ "print": lambda *args: None,
+ }
+ with bind_invocation(host, api):
+ await execute(block, namespace)
+ for name, data in host.calls:
+ if name in {"reply_message", "set_query_var"}:
+ expected_query = 42 if "event_context" in block else 43
+ assert data["query_id"] == expected_query
+ assert "run_id" not in data
+ if "reply.update" in block:
+ ops = [
+ data["operation"]
+ for name, data in host.calls
+ if name == "reply_stream"
+ ]
+ assert ops == ["update", "update", "finish"], ops
+ if "older =" in block:
+ pages = [data for name, data in host.calls if name == "history_page"]
+ assert len(pages) == 2 and pages[1]["before_cursor"] == "older"
+ if "except PermissionDeniedError" in block:
+ # Exercise both documented error handlers with the real SDK proxy.
+ api._allowed_tool_names = frozenset()
+ await execute(block, namespace)
+ ctx.log.assert_awaited()
+ api._allowed_tool_names = frozenset({"event_get_actor"})
+ host.call_action = AsyncMock(
+ side_effect=AgentAPIException(
+ AgentAPIError(
+ code="host.action_error", message="Failed", retryable=False
+ )
+ )
+ )
+ await execute(block, namespace)
+ assert ctx.log.await_count == 2
+ total += 1
+ print(
+ f"{total} API examples passed; all three locales have identical code. Host delivery is mocked."
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/scripts/verify-runner-tutorial.py b/scripts/verify-runner-tutorial.py
new file mode 100644
index 000000000..dfb27b2df
--- /dev/null
+++ b/scripts/verify-runner-tutorial.py
@@ -0,0 +1,167 @@
+"""Execute the published tutorial with the matching 4.11 SDK installed.
+
+Run: python scripts/verify-runner-tutorial.py
+The tool API is mocked; real Host/runtime checks are a separate validation layer.
+"""
+
+import asyncio
+import re
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, Mock
+
+import yaml
+from langbot_plugin.api.definition.components.runner import RunnerContext
+
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def load_tutorial():
+ examples = []
+ for locale in ("zh", "en", "ja"):
+ path = ROOT / locale / "plugin/dev/components/runner.mdx"
+ source = path.read_text()
+ python = re.findall(r"^```python\n(.*?)^```", source, re.M | re.S)
+ manifests = [
+ block
+ for block in re.findall(r"^```yaml\n(.*?)^```", source, re.M | re.S)
+ if "kind: Runner" in block
+ ]
+ python = [block for block in python if "class Welcome(Runner):" in block]
+ assert len(python) == len(manifests) == 1, path
+ examples.append((python[0], manifests[0]))
+ assert examples[0] == examples[1] == examples[2], "Locale examples differ"
+ code, manifest = examples[0]
+ namespace = {}
+ exec(compile(code, "runner.mdx", "exec"), namespace)
+ manifest = yaml.safe_load(manifest)
+ assert manifest["spec"]["events"] == ["group.member_joined", "message.received"]
+ assert manifest["spec"]["capabilities"]["tool_calling"] is True
+ assert manifest["spec"]["permissions"]["tools"] == ["detail", "call"]
+ defaults = {field["name"]: field["default"] for field in manifest["spec"]["config"]}
+ return namespace[manifest["execution"]["python"]["attr"]], defaults
+
+
+async def main():
+ component_class, defaults = load_tutorial()
+ component = component_class()
+ await component.initialize()
+ cases = [
+ (
+ "join",
+ {
+ "type": "group.member_joined",
+ "member": {"id": "alice", "nickname": "Alice"},
+ },
+ {},
+ "Alice, Welcome aboard!",
+ ),
+ (
+ "nickname-fallback",
+ {"type": "group.member_joined", "member": {"id": "alice"}},
+ {},
+ "alice, Welcome aboard!",
+ ),
+ (
+ "join-disabled",
+ {"type": "group.member_joined"},
+ {"reply_enabled": False},
+ None,
+ ),
+ (
+ "command-disabled",
+ {
+ "type": "message.received",
+ "message_chain": [{"type": "Plain", "text": "/hello"}],
+ },
+ {"reply_enabled": False},
+ None,
+ ),
+ (
+ "ordinary-text",
+ {
+ "type": "message.received",
+ "message_chain": [{"type": "Plain", "text": "hello"}],
+ },
+ {},
+ None,
+ ),
+ ("empty-message", {"type": "message.received", "message_chain": []}, {}, None),
+ (
+ "image-only",
+ {
+ "type": "message.received",
+ "message_chain": [
+ {"type": "Image", "url": "https://example.com/image.png"}
+ ],
+ },
+ {},
+ None,
+ ),
+ ]
+ for chat_type in ("private", "group"):
+ cases.append(
+ (
+ chat_type,
+ {
+ "type": "message.received",
+ "chat_type": chat_type,
+ "message_chain": [{"type": "Plain", "text": " /hello "}],
+ },
+ {"greeting": "你好 🧩"},
+ "你好 🧩",
+ )
+ )
+
+ async def execute(name, data, config, reply, *, failure=False):
+ api = SimpleNamespace(
+ call_tool=AsyncMock(
+ side_effect=RuntimeError("Delivery failed") if failure else None,
+ return_value={"mock": True},
+ )
+ )
+ component.get_run_api = Mock(return_value=api)
+ context = RunnerContext.model_validate({
+ "run_id": name, "trigger": {"type": data["type"]},
+ "event": {"event_id": name, "event_type": data["type"], "source": "test", "data": data},
+ "config": {**defaults, **config}, "input": {}, "delivery": {"surface": "test"}, "resources": {}, "runtime": {},
+ })
+ results = []
+ try:
+ async for result in component.invoke(context):
+ results.append(result)
+ except RuntimeError as exc:
+ assert failure and str(exc) == "Delivery failed"
+ else:
+ assert not failure, "Delivery failure was swallowed"
+ assert sum(r.type == "run.completed" for r in results) == int(not failure)
+ assert all(r.run_id == name for r in results)
+ if reply is None:
+ api.call_tool.assert_not_called()
+ else:
+ api.call_tool.assert_awaited_once_with("event_reply", {"text": reply})
+ started = [
+ r.data["tool_call_id"] for r in results if r.type == "tool.call.started"
+ ]
+ completed = [
+ r.data["tool_call_id"]
+ for r in results
+ if r.type == "tool.call.completed"
+ ]
+ assert len(started) == 1 and started == completed
+ if failure:
+ assert any(r.type == "processor.log" for r in results)
+ assert any(r.data.get("error") == "Delivery failed" for r in results)
+ print(f"PASS {name}")
+
+ for case in cases:
+ await execute(*case)
+ await execute("delivery-failure", cases[0][1], {}, cases[0][3], failure=True)
+ print(
+ "All 10 tutorial cases passed; Chinese, English and Japanese code is identical."
+ )
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/sitemap-alternates.xml b/sitemap-alternates.xml
index 86f94335d..3f403ce98 100644
--- a/sitemap-alternates.xml
+++ b/sitemap-alternates.xml
@@ -1953,6 +1953,27 @@
+
+ https://langbot.app/docs/en/plugin/dev/apis/agent-run
+
+
+
+
+
+
+ https://langbot.app/docs/zh/plugin/dev/apis/agent-run
+
+
+
+
+
+
+ https://langbot.app/docs/ja/plugin/dev/apis/agent-run
+
+
+
+
+
https://langbot.app/docs/en/plugin/dev/apis/common
@@ -2016,6 +2037,27 @@
+
+ https://langbot.app/docs/en/plugin/dev/apis/platform
+
+
+
+
+
+
+ https://langbot.app/docs/zh/plugin/dev/apis/platform
+
+
+
+
+
+
+ https://langbot.app/docs/ja/plugin/dev/apis/platform
+
+
+
+
+
https://langbot.app/docs/en/plugin/dev/apis/tech-details
@@ -2205,6 +2247,27 @@
+
+ https://langbot.app/docs/en/plugin/dev/components/runner
+
+
+
+
+
+
+ https://langbot.app/docs/zh/plugin/dev/components/runner
+
+
+
+
+
+
+ https://langbot.app/docs/ja/plugin/dev/components/runner
+
+
+
+
+
https://langbot.app/docs/en/plugin/dev/components/tool
diff --git a/tests/agent-static-build.test.mjs b/tests/agent-static-build.test.mjs
index 9ee1c33f0..7cadfa91a 100644
--- a/tests/agent-static-build.test.mjs
+++ b/tests/agent-static-build.test.mjs
@@ -22,7 +22,7 @@ function links(node, result = []) {
}
test("every sitemap page has meaningful canonical Markdown and HTML discovery", async () => {
- assert.equal(urls.length, 485);
+ assert.equal(urls.length, 494);
for (const url of urls) {
const route = relative(url);
const text = await read(`${route}.md`);
diff --git a/tests/fumapress-migration.test.mjs b/tests/fumapress-migration.test.mjs
index a99888ae5..0988efc92 100644
--- a/tests/fumapress-migration.test.mjs
+++ b/tests/fumapress-migration.test.mjs
@@ -170,8 +170,8 @@ test("MDX compatibility normalization removes internal .html route suffixes", ()
test("canonical MDX and nested OpenAPI sources are discovered", async () => {
const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8"));
const pages = await collectMdxDocuments(root);
- assert.equal(pages.length, 308);
- assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 98, zh: 112, ja: 98 });
+ assert.equal(pages.length, 317);
+ assert.deepEqual(Object.fromEntries([...pages.reduce((acc, page) => acc.set(page.split("/", 1)[0], (acc.get(page.split("/", 1)[0]) ?? 0) + 1), new Map()).entries()]), { en: 101, zh: 115, ja: 101 });
assert.deepEqual(collectOpenApiSources(docs), [
{ locale: "en", source: "openapi/service-api-en.json", directory: "en/api-reference" },
{ locale: "zh", source: "openapi/service-api-zh.json", directory: "zh/api-reference" },
@@ -216,7 +216,7 @@ test("prebuild is deterministic and preserves local assets and SEO files", async
const first = await prepareFumapress({ root, outRoot: temp });
const second = await prepareFumapress({ root, outRoot: temp });
assert.deepEqual(first, second);
- assert.equal(first.documents, 308);
+ assert.equal(first.documents, 317);
assert.equal(first.fallbackDefaults, 0);
assert.equal(first.localeOnlyDocuments, 14);
assert.equal(first.redirects, 103);
diff --git a/tests/fumapress-static-build.test.mjs b/tests/fumapress-static-build.test.mjs
index a371ca471..040450ab1 100644
--- a/tests/fumapress-static-build.test.mjs
+++ b/tests/fumapress-static-build.test.mjs
@@ -43,9 +43,9 @@ function staticHtmlForUrlPath(urlPath) {
return path.join(publicRoot, ...parts, "index.html");
}
-test("all 308 canonical localized documents have static HTML", async () => {
+test("all canonical localized documents have static HTML", async () => {
const documents = await collectMdxDocuments(root);
- assert.equal(documents.length, 308);
+ assert.equal(documents.length, 317);
const missing = [];
for (const document of documents) {
try {
@@ -193,7 +193,7 @@ test("all locales emit the OpenAPI surface with endpoint semantics", async () =>
}
});
-test("canonical sitemap routes match the legacy Mintlify directory structure", async () => {
+test("canonical sitemap preserves legacy routes and includes current documents", async () => {
const legacy = new Set((await readFile(path.join(root, "tests/fixtures/legacy-sitemap-routes.txt"), "utf8"))
.trim().split("\n").map((route) => route.replace(/\/$/, "")));
// Mintlify accidentally indexed this repository maintenance note; preserve
@@ -207,7 +207,11 @@ test("canonical sitemap routes match the legacy Mintlify directory structure", a
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
const actual = new Set([...sitemap.matchAll(/https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)]
.map((match) => decodeURIComponent(match[1]).replace(/\/$/, "")));
- assert.deepEqual([...actual].sort(), [...legacy].sort());
+ const expected = new Set(legacy);
+ for (const document of await collectMdxDocuments(root)) {
+ expected.add(`/${document.replace(/\.mdx$/, "").replace(/\/index$/, "")}`);
+ }
+ assert.deepEqual([...actual].sort(), [...expected].sort());
});
const locales = ["en", "zh", "ja"];
diff --git a/tests/locale-static-build.test.mjs b/tests/locale-static-build.test.mjs
index 3cd370884..d4fa8d520 100644
--- a/tests/locale-static-build.test.mjs
+++ b/tests/locale-static-build.test.mjs
@@ -35,6 +35,6 @@ test("every localized HTML payload includes the preference client provider", asy
}
}
for (const locale of ["en", "zh", "ja"]) await visit(path.join(root, locale));
- // 485 canonical content pages plus one framework 404 page per locale.
- assert.equal(count, 488);
+ // 494 canonical content pages plus one framework 404 page per locale.
+ assert.equal(count, 497);
});
diff --git a/zh/articles/dify-agent-discord-telegram-slack.mdx b/zh/articles/dify-agent-discord-telegram-slack.mdx
index eefc7b978..558ba941e 100644
--- a/zh/articles/dify-agent-discord-telegram-slack.mdx
+++ b/zh/articles/dify-agent-discord-telegram-slack.mdx
@@ -76,9 +76,9 @@ Please always maintain a friendly and professional attitude. If you need to use

-选择 **Dify Service API** 作为 Runner:
+选择 **Dify Service API** 作为运行器:
-
+
填写配置:
- **Base URL**: `https://api.dify.ai/v1`
diff --git a/zh/articles/langflow-drag-and-drop-ai-bot.mdx b/zh/articles/langflow-drag-and-drop-ai-bot.mdx
index 3ef8137c2..8f832f693 100644
--- a/zh/articles/langflow-drag-and-drop-ai-bot.mdx
+++ b/zh/articles/langflow-drag-and-drop-ai-bot.mdx
@@ -120,9 +120,9 @@ http://localhost:7860/flow/{flow-id}

-配置 **Runner**,选择 **Langflow API**:
+配置**运行器**,选择 **Langflow API**:
-
+
填写 Langflow 配置:
diff --git a/zh/articles/n8n-multi-platform-ai-chatbot.mdx b/zh/articles/n8n-multi-platform-ai-chatbot.mdx
index 1b082d3f5..ae848000a 100644
--- a/zh/articles/n8n-multi-platform-ai-chatbot.mdx
+++ b/zh/articles/n8n-multi-platform-ai-chatbot.mdx
@@ -101,7 +101,7 @@ http://localhost:5678/webhook/{your-webhook-id}

-切换到 **AI** 标签页,在 Runner 下拉菜单中选择 "n8n Workflow API"。配置:
+切换到 **AI** 标签页,在运行器下拉菜单中选择 "n8n Workflow API"。配置:
- **Webhook URL**:粘贴你的 n8n 生产环境 webhook URL
- **Authentication Type**:None(与 n8n webhook 设置保持一致)
diff --git a/zh/plugin/dev/apis/agent-run.mdx b/zh/plugin/dev/apis/agent-run.mdx
new file mode 100644
index 000000000..e04d76f9b
--- /dev/null
+++ b/zh/plugin/dev/apis/agent-run.mdx
@@ -0,0 +1,263 @@
+---
+title: "上下文 API"
+---
+
+上下文保存当前一次组件调用的数据。不同组件接收的上下文不同,请先按组件找到对应入口。
+
+| 组件 | 处理函数入口 | 上下文类型 |
+| --- | --- | --- |
+| EventListener | `handler(event_context)` | `EventContext` |
+| Command | `subcommand(context)` | `ExecuteContext` |
+| Tool | `call(params, session, query_id)` | 无独立上下文对象 |
+| 运行器 | `run(ctx)` 或 `handler(ctx)` | `RunnerContext` |
+| KnowledgeEngine | `ingest(context)`、`retrieve(context)` | `IngestionContext`、`RetrievalContext` |
+| KnowledgeRetriever | `retrieve(context)` | `RetrievalContext` |
+| Parser | `parse(context)` | `ParseContext` |
+| Page | `handle_api(request)` | `PageRequest` |
+
+本页只介绍上下文中的数据和方法。模型、知识库、存储等 LangBot 能力统一通过 `self.plugin` 调用,见 [LangBot API](/zh/plugin/dev/apis/common);平台操作见[平台 API](/zh/plugin/dev/apis/platform)。
+
+`EventContext`、`ExecuteContext` 和 `Session` 还会携带 `instance_uuid`、`workspace_uuid` 和 `placement_generation`。这些字段用于标识当前执行所在的实例和工作区,不应作为插件自行鉴权的依据。
+
+## EventListener
+
+EventListener 处理流水线事件。事件处理函数接收 `EventContext`:
+
+```python
+event = event_context.event
+sender_id = event.sender_id
+```
+
+### 上下文数据
+
+| 属性 | 类型 | 说明 |
+| --- | --- | --- |
+| `event` | `BaseEventModel` | 当前事件对象,实际类型与注册的事件类型一致 |
+| `event_name` | `str` | 当前事件类名 |
+| `query_id` | `int` | 当前流水线请求 ID |
+| `query_uuid` | `str \| None` | 当前流水线请求的稳定标识 |
+| `eid` | `int` | Plugin Runtime 内的临时事件编号,不应持久化 |
+| `is_prevent_default` | `bool` | 是否已阻止默认处理;通过 `prevent_default()` 修改 |
+| `is_prevent_postorder` | `bool` | 是否已阻止后续插件;通过 `prevent_postorder()` 修改 |
+
+事件对象的字段和所有可监听事件见[流水线事件](/zh/plugin/dev/apis/pipeline-events)。
+
+### 上下文方法
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await event_context.reply(message_chain, quote_origin=False)` | - | 回复当前请求所在会话 |
+| `await event_context.get_bot_uuid()` | `str` | 获取当前请求来源机器人 UUID |
+| `await event_context.set_query_var(key, value)` | - | 设置当前请求变量 |
+| `await event_context.get_query_var(key)` | `Any` | 获取一个请求变量 |
+| `await event_context.get_query_vars()` | `dict[str, Any]` | 获取全部请求变量 |
+| `await event_context.create_new_conversation()` | `dict[str, Any]` | 清除流水线当前使用的对话,后续请求会创建新对话 |
+| `await event_context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 列出当前流水线 Local Agent 绑定的知识库 |
+| `await event_context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 检索当前流水线已绑定的知识库 |
+| `event_context.prevent_default()` | - | 阻止当前事件的默认处理 |
+| `event_context.prevent_postorder()` | - | 阻止后续插件继续处理当前事件 |
+
+`reply()` 使用 [MessageChain](/zh/plugin/dev/apis/messages)。请求变量、回复和流水线知识库方法依赖当前请求;没有关联请求的事件不能调用这些方法。`retrieve_knowledge()` 的 `kb_id` 必须来自 `list_pipeline_knowledge_bases()`。
+
+`prevent_default()` 只对支持中止默认流程的流水线事件生效,具体事件见[流水线事件](/zh/plugin/dev/apis/pipeline-events#阻止默认行为)。
+
+## Command
+
+Command 的子命令处理函数接收 `ExecuteContext`:
+
+```python
+query = " ".join(context.crt_params)
+session = context.session
+```
+
+### 上下文数据
+
+| 属性 | 类型 | 说明 |
+| --- | --- | --- |
+| `session` | `Session` | 当前消息所属会话 |
+| `command_text` | `str` | 去掉命令前缀后的完整文本 |
+| `full_command_text` | `str` | 包含命令前缀的完整文本 |
+| `command` | `str` | 根命令名称 |
+| `crt_command` | `str` | 当前正在执行的子命令名称 |
+| `params` | `list[str]` | 根命令后的全部参数 |
+| `crt_params` | `list[str]` | 当前子命令尚未消费的参数 |
+| `privilege` | `int` | 当前用户的命令权限级别 |
+| `query_id` | `int` | 当前流水线请求 ID |
+| `query_uuid` | `str \| None` | 当前流水线请求的稳定标识 |
+
+`context.shift()` 由 SDK 在进入当前子命令前调用,组件不需要手动调用。
+
+### 上下文方法
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await context.reply(message_chain, quote_origin=False)` | - | 回复当前请求所在会话 |
+| `await context.get_bot_uuid()` | `str` | 获取当前请求来源机器人 UUID |
+| `await context.set_query_var(key, value)` | - | 设置当前请求变量 |
+| `await context.get_query_var(key)` | `Any` | 获取一个请求变量 |
+| `await context.get_query_vars()` | `dict[str, Any]` | 获取全部请求变量 |
+| `await context.create_new_conversation()` | `dict[str, Any]` | 清除流水线当前使用的对话,后续请求会创建新对话 |
+| `await context.list_pipeline_knowledge_bases()` | `list[dict[str, Any]]` | 列出当前流水线 Local Agent 绑定的知识库 |
+| `await context.retrieve_knowledge(kb_id, query_text, top_k=5, filters=None)` | `list[dict[str, Any]]` | 检索当前流水线已绑定的知识库 |
+
+这些请求方法的行为与 EventListener 相同。命令的注册和返回值见 [Command 组件](/zh/plugin/dev/components/command)。
+
+## Tool
+
+Tool 没有独立的上下文对象。调用信息直接传入 `call()`:
+
+```python
+conversation_type = session.launcher_type.value
+conversation_id = session.launcher_id
+sender_id = session.sender_id
+```
+
+| 参数 | 说明 |
+| --- | --- |
+| `params` | 模型或调用方根据工具 JSON Schema 生成的参数 |
+| `session` | 当前会话数据 |
+| `query_id` | 当前流水线请求 ID;调用需要请求上下文的 LangBot API 时原样传入 |
+
+`Session` 的常用字段如下:
+
+| 属性 | 说明 |
+| --- | --- |
+| `launcher_type` | 会话类型:`person` 或 `group` |
+| `launcher_id` | 私聊用户或群组 ID |
+| `sender_id` | 当前消息发送者 ID |
+| `bot_uuid` | 当前机器人 UUID |
+| `using_conversation` | 当前流水线会话 |
+| `conversations` | 该 Session 保存的流水线会话列表 |
+| `use_prompt_name` | 当前使用的提示词名称 |
+| `create_time`、`update_time` | Session 的创建时间和更新时间 |
+
+`session` 只保存数据,不提供 `reply()` 等上下文方法。工具的返回值交给调用方,不会自动发送到聊天平台。需要调用 LangBot 或平台能力时使用 `self.plugin`;相关接口要求 `session` 和 `query_id` 时,传入本次调用收到的值。
+
+
+
+## 运行器
+
+SDK `Runner` 类型的自定义 `run(ctx)` 和事件处理函数都接收 `RunnerContext`。该上下文只在本次运行期间有效。
+
+```python
+text = ctx.input.to_text()
+event_type = ctx.event.event_type
+```
+
+### 上下文数据
+
+| 属性 | 类型 | 说明 |
+| --- | --- | --- |
+| `run_id` | `str` | 本次运行 ID |
+| `trigger` | `AgentTrigger` | 触发类型、来源和时间 |
+| `event` | `AgentEventContext` | 标准事件信封,包含事件 ID、类型、来源、时间和原始数据 |
+| `platform_event` | 平台事件类型 | 按事件类型解析后的平台事件对象 |
+| `conversation` | `ConversationContext \| None` | 当前会话、线程、机器人和工作区标识 |
+| `actor` | `ActorContext \| None` | 事件发起者 |
+| `subject` | `SubjectContext \| None` | 事件所作用的消息、群组等对象 |
+| `input` | `AgentInput` | 当前输入的文本、结构化内容和附件 |
+| `delivery` | `DeliveryContext` | 当前输出目标及流式、编辑、回应等能力 |
+| `resources` | `AgentResources` | 本次运行已授权的模型、工具、知识库、技能和存储 |
+| `context` | `ContextAccess` | 会话游标、内联范围和可用上下文 API |
+| `state` | `AgentRunState` | 本次运行开始时读取到的各作用域状态快照 |
+| `runtime` | `AgentRuntimeContext` | LangBot 版本、追踪 ID 和截止时间 |
+| `config` | `dict[str, Any]` | 当前 Agent 或插件处理器配置 |
+| `adapter` | `AdapterContext \| None` | 入口适配器附加数据 |
+| `variables` | `dict[str, Any]` | 公开的请求变量,可用于运行器插值模板 |
+| `metadata` | `dict[str, Any]` | 宿主提供的其他元数据 |
+
+优先使用上表中的稳定字段。`event.data` 和 `adapter.extra` 用于承载尚未进入稳定字段的附加信息。
+
+### 回复与日志
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await ctx.get_bot_uuid()` | `str` | 获取来源机器人 UUID;无关联机器人时抛出异常 |
+| `await ctx.reply(message_chain, quote_origin=False)` | `Any` | 回复当前事件,支持字符串或 `MessageChain` |
+| `ctx.reply_stream()` | 异步上下文管理器 | 流式更新同一条回复 |
+| `await ctx.log(text, level="info")` | - | 写入本次运行日志;级别支持 `debug`、`info`、`warning`、`error` |
+
+`reply_stream()` 的 `update(text)` 接收当前完整文本。平台不支持流式时,LangBot 会在结束时发送一条完整消息;调试模式只模拟发送。
+
+### 可用工具
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await ctx.get_available_tools()` | `list[dict[str, Any]]` | 获取本次实际允许调用的上下文动作、平台 API、插件工具和 MCP 工具 |
+| `await ctx.call_tool(tool_name, parameters=None)` | `dict[str, Any]` | 调用上述列表中的工具 |
+
+`get_available_tools()` 的每一项包含 `name`、`description`、`parameters`、`type` 和 `operations`。返回结果已经按当前 Agent 或插件处理器的配置和事件能力过滤;不在列表中的工具不能通过 `ctx.call_tool()` 调用。
+
+### Box 与附件
+
+Box 是否启用、何时创建以及如何复用,由运行器决定。获取状态和申请 Box 使用 [LangBot API](/zh/plugin/dev/apis/common#box-沙箱);当前运行的绑定和附件操作使用下列方法。
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await ctx.bind_box(box_id)` | `BoxBinding` | 绑定当前工作区中的 Box,返回 `box_id`、`outbox`;同次运行不能切换 Box |
+| `await ctx.import_box_attachments(attachment_ids=None)` | `list[BoxFile]` | 导入 `ctx.input.attachments` 的 `ref`,省略参数则导入全部;返回各文件的沙箱 `path` |
+| `await ctx.export_box_files()` | `list[BoxFile]` | 导出本次运行 outbox 中的文件,返回文件句柄,不发送消息 |
+| `await ctx.reply_files(file_ids)` | `Any` | 使用导出的 `id` 回复当前事件,受回复工具权限约束 |
+
+`BoxFile` 包含 `id`、`name`、`type`、`size` 和可选的 `path`。输入的 `url`、`content` 等原始信息仍保留,只有显式导入才产生沙箱文件路径。文件句柄仅在本次运行有效,不能重复发送。
+
+先绑定 Box,再使用原生执行、文件工具和附件方法。复用同一 Box 时,各次运行的附件目录仍独立。`ctx.variables` 提供公开的请求变量,运行器可据此计算复用键。
+
+通过流水线运行时,`ctx.delivery.automatic_reply` 为 `True`,可以在 `RunnerResult.message_completed(ctx.run_id, message, file_ids=[...])` 返回附件;Agent 和插件处理器使用 `ctx.reply_files()` 显式发送。
+
+外部运行器通过 `AgentRunExternalTools` 工具网关使用相同流程:`langbot_get_box_status`、`langbot_list_boxes`、`langbot_acquire_box`、`langbot_bind_box`、`langbot_import_box_attachments`、`langbot_export_box_files`。具有当前事件回复权限时还可使用 `langbot_reply_files`;这些调用均携带当前运行的授权。
+
+### 提示词、历史与事件
+
+| 方法 | 返回值 | 可用性字段 |
+| --- | --- | --- |
+| `await ctx.get_prompt()` | `list[dict[str, Any]]` | `prompt_get` |
+| `await ctx.history_page(conversation_id=None, before_cursor=None, after_cursor=None, limit=50, direction="backward", include_attachments=False)` | `HistoryPage` | `history_page` |
+| `await ctx.history_search(query, filters=None, top_k=10)` | `HistorySearchResult` | `history_search` |
+| `await ctx.event_get(event_id)` | `AgentEventRecord` | `event_get` |
+| `await ctx.event_page(conversation_id=None, event_types=None, before_cursor=None, limit=50)` | `EventPage` | `event_page` |
+| `await ctx.steering_pull(mode="all", limit=None)` | `SteeringPullResult` | `steering_pull` |
+
+### 状态
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await ctx.state_get(scope, key)` | `dict[str, Any]` | 读取状态值 |
+| `await ctx.state_set(scope, key, value)` | `dict[str, Any]` | 写入可 JSON 序列化的状态值 |
+| `await ctx.state_delete(scope, key)` | `dict[str, Any]` | 删除状态值 |
+| `await ctx.state_list(scope, prefix=None, limit=100)` | `dict[str, Any]` | 列出指定作用域的状态键 |
+
+`scope` 支持 `conversation`、`actor`、`subject` 和 `runner`。这些方法要求 `ctx.context.available_apis.state` 为 `true`。
+
+### 运行记录
+
+| 方法 | 返回值 | 可用性字段 |
+| --- | --- | --- |
+| `await ctx.run_get(run_id=None)` | `AgentRun` | `run_get` |
+| `await ctx.run_list(conversation_id=None, statuses=None, before_cursor=None, limit=50)` | `RunPage` | `run_list` |
+| `await ctx.run_events_page(run_id=None, before_cursor=None, after_cursor=None, limit=50, direction="forward")` | `RunEventPage` | `run_events_page` |
+| `await ctx.run_cancel(run_id=None, reason=None)` | `AgentRun` | `run_cancel` |
+| `await ctx.run_append_result(result)` | `AgentRunEvent` | `run_append_result` |
+| `await ctx.run_finalize(run_id=None, status=None, reason=None)` | `AgentRun` | `run_finalize` |
+
+`run_id=None` 表示当前运行。普通事件处理函数结束后,SDK 会自动完成本次运行;自定义 `run(ctx)` 通过 `yield RunnerResult` 返回结果时,也不需要再调用 `run_append_result()` 重复登记。
+
+### 可用性与授权
+
+通过 `ctx.context.available_apis` 判断提示词、历史、事件、状态和运行记录接口是否可用。模型、工具和知识库的授权范围分别位于 `ctx.resources.models`、`ctx.resources.tools` 和 `ctx.resources.knowledge_bases`。
+
+运行器执行期间,通过 `self.plugin` 发起的模型、工具、知识库、存储和平台调用会自动关联当前运行,并按 `ctx.resources` 再次校验,无需传入 `run_id`。处理函数结束后,不要继续在后台任务中使用本次上下文或这些运行关联调用。
+
+`ctx.api` 是底层的运行级代理。普通组件代码应优先使用本页列出的 `ctx` 方法,并通过 `self.plugin` 调用 LangBot API。
+
+## 数据型上下文
+
+以下组件收到的是本次任务的输入数据,不提供 EventListener 或运行器的上下文方法。字段、返回值和完整示例放在对应组件教程中。
+
+| 组件入口 | 主要数据 | 组件教程 |
+| --- | --- | --- |
+| `KnowledgeEngine.ingest(context: IngestionContext)` | 文件、目标知识库、创建配置和解析结果 | [KnowledgeEngine](/zh/plugin/dev/components/knowledge-engine#文档摄取) |
+| `KnowledgeEngine.retrieve(context: RetrievalContext)` | 查询、目标知识库、检索配置和过滤条件 | [KnowledgeEngine](/zh/plugin/dev/components/knowledge-engine#知识检索) |
+| `KnowledgeRetriever.retrieve(context: RetrievalContext)` | 查询和外部知识库配置 | [KnowledgeRetriever](/zh/plugin/dev/components/knowledge-retriever#检索上下文) |
+| `Parser.parse(context: ParseContext)` | 文件内容、文件名、MIME 类型和元数据 | [Parser](/zh/plugin/dev/components/parser#解析方法) |
+| `Page.handle_api(request: PageRequest)` | 接口路径、HTTP 方法、请求体和请求头 | [Page](/zh/plugin/dev/components/page#pagerequest-字段) |
diff --git a/zh/plugin/dev/apis/common.mdx b/zh/plugin/dev/apis/common.mdx
index 71dd51fa1..950558c49 100644
--- a/zh/plugin/dev/apis/common.mdx
+++ b/zh/plugin/dev/apis/common.mdx
@@ -1,116 +1,15 @@
---
-title: "插件通用 API"
+title: "LangBot API"
---
-LangBot 为插件提供了一系列 API ,用于操作 LangBot 各个模块、控制消息上下文等。
-
-## 请求 API
-
-操作当前正在处理的用户请求(消息)。仅在`EventListener`和`Command`组件中可用。访问方式如下:
-
-- `EventListener` 的各个事件处理器方法中:`event_context: context.EventContext`对象内部方法
-- `Command` 的各个子命令处理器方法中:`context: ExecuteContext`对象内部方法
-
-### 直接回复消息
-
-直接回复一个消息链到当前请求所在会话。
-
-消息链构造方式请参考[消息平台实体](/zh/plugin/dev/apis/messages)。
-
-```python
-async def reply(
- self, message_chain: platform_message.MessageChain, quote_origin: bool = False
-):
- """Reply to the message request
-
- Args:
- message_chain (platform.types.MessageChain): LangBot message chain
- quote_origin (bool): Whether to quote the original message
- """
-
-# 调用示例
-await event_context.reply(
- platform_message.MessageChain([
- platform_message.Plain(text="Hello, world!"),
- ]),
-)
-```
-
-### 获取机器人 UUID
-
-获取当前请求来源机器人 UUID。
-
-```python
-async def get_bot_uuid(self) -> str:
- """Get the bot uuid"""
-
-# 调用示例
-bot_uuid = await event_context.get_bot_uuid()
-```
-
-### 设置请求变量
-
-单次请求中某些信息会被存储到请求变量中,在使用 Dify 等外部 Agent 平台时,[这些变量会显式传入 Agent 平台](/zh/deploy/pipelines/readme.html#%E8%AF%B7%E6%B1%82%E5%8F%98%E9%87%8F)。
-
-```python
-async def set_query_var(self, key: str, value: Any):
- """Set a query variable"""
-
-# 调用示例
-await event_context.set_query_var("key", "value")
-```
-
-### 获取请求变量
-
-获取单个请求变量。
-
-```python
-async def get_query_var(self, key: str) -> Any:
- """Get a query variable"""
-
-# 调用示例
-query_var = await event_context.get_query_var("key")
-```
-
-### 获取所有请求变量
-
-```python
-async def get_query_vars(self) -> dict[str, Any]:
- """Get all query variables"""
-
-# 调用示例
-query_vars = await event_context.get_query_vars()
-```
-
-### 获取当前流水线配置的知识库列表
-
-获取当前请求所使用流水线中配置的知识库列表。
-
-```python
-async def list_pipeline_knowledge_bases(self) -> list[dict[str, Any]]:
- """List knowledge bases configured for the current pipeline"""
-
-# 调用示例
-knowledge_bases = await event_context.list_pipeline_knowledge_bases()
-
-# 返回示例
-[
- {
- "uuid": "kb_uuid",
- "name": "Product Docs",
- "description": "内部产品文档",
- }
-]
-```
-
-仅返回当前请求对应流水线在 `Local Agent` 配置中绑定的知识库。若当前流水线未配置知识库,或未使用 `Local Agent`,则返回空列表。
+模型、工具、知识库和存储等能力通过 `self.plugin` 访问。当前事件、回复和会话状态的访问方式见[上下文 API](/zh/plugin/dev/apis/agent-run)。
## LangBot API
-这些 API 可在插件任何组件中调用。访问方式如下:
+这些 API 可在所有组件中通过 `self.plugin` 调用。访问方式如下:
- 插件根目录`main.py`中:`self` 对象内部方法,这些 API 均由插件类父类 `BasePlugin` 提供。
-- 插件任何组件类中:`self.plugin` 对象内部方法。
+- 组件类中:`self.plugin` 对象内部方法。
### 获取插件配置信息
@@ -224,6 +123,9 @@ async def invoke_llm(
messages: list[provider_message.Message],
funcs: list[resource_tool.LLMTool] = [],
extra_args: dict[str, Any] = {},
+ timeout: float | None = None,
+ *,
+ reasoning_level: ReasoningLevel | None = None,
) -> provider_message.Message:
"""Invoke an LLM model"""
@@ -236,6 +138,65 @@ llm_message = await self.plugin.invoke_llm(
)
```
+
+### 思考等级
+
+上述模型调用方法都支持可选的关键字参数 `reasoning_level`:
+
+```python
+response = await self.plugin.invoke_llm(
+ llm_model_uuid=model_uuid,
+ messages=[provider_message.Message(role="user", content="分析这个问题")],
+ reasoning_level="medium",
+)
+```
+
+可选值为 `provider_default`、`disabled`、`enabled`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`,具体取决于模型支持的等级。LangBot 校验能力并转换为供应商参数,不支持的等级会报错。
+
+- 不传或传 `None`:保持原有行为,使用模型配置及 `extra_args`。
+- 传 `provider_default`:不额外指定思考等级。
+- 传其他等级:仅作用于本次调用;不要再通过 `extra_args` 重复设置供应商的思考参数。
+
+Runner 应读取自己的配置,在每次模型调用时显式传入对应等级,包括备用模型和工具调用后的续答。LangBot 不会自动读取 Runner 配置中的思考等级。旧插件无需修改;显式使用新参数时,如果 Host 不支持,SDK 会在调用模型前提示升级。
+
+### 模型流式输出与用量
+
+| 方法 | 返回值 |
+| --- | --- |
+| `invoke_llm_with_usage(llm_model_uuid, messages, funcs=[], extra_args={}, timeout=None, *, reasoning_level=None)` | `LLMInvokeResult` |
+| `invoke_llm_stream(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | 异步迭代 `MessageChunk` |
+| `invoke_llm_stream_events(llm_model_uuid, messages, funcs=[], extra_args={}, *, reasoning_level=None)` | 异步迭代 `LLMStreamEvent` |
+
+
+参数与 `invoke_llm` 相同。`timeout` 单位为秒,非流式调用默认 120 秒。需要 token 用量时使用 `invoke_llm_with_usage`:
+
+```python
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+result = await self.plugin.invoke_llm_with_usage(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+)
+print(result.message.content)
+if result.usage is not None:
+ print(result.usage.total_tokens)
+```
+
+`usage` 可为 `None`,其中 `prompt_tokens`、`completion_tokens`、`total_tokens` 也可能缺失,不代表用量为零。供应商附加的缓存等用量字段会保留。
+
+```python
+async for event in self.plugin.invoke_llm_stream_events(
+ llm_model_uuid=model_uuid,
+ messages=[Message(role="user", content="Hello!")],
+):
+ if event.chunk is not None:
+ print(event.chunk.content)
+ if event.usage is not None:
+ print(event.usage.model_dump())
+```
+
+`LLMStreamEvent` 包含可选的 `chunk` 和 `usage`,末尾可能只有用量,没有文本。`invoke_llm_stream` 只返回文本或工具调用分块。`MessageChunk.content` 是当前片段,`all_content` 在提供时是累计文本;累计文本应用替换,增量文本才追加;`is_final` 为真时也可能仍有文本。模型流式调用不会直接发送平台消息,主动流式回复见[运行上下文 API](/zh/plugin/dev/apis/agent-run)。
+
### 列出可用解析器
列出宿主当前可用的 Parser 插件,可按 MIME 类型过滤。
@@ -428,7 +389,7 @@ file_bytes = await self.plugin.get_config_file("key")
## 知识库 API
-这些 API 可在任意组件中通过 `self.plugin` 访问,用于列出和检索 LangBot 实例中的所有知识库,不受流水线配置限制。
+这些 API 可在提供 `self.plugin` 的普通组件中访问,用于列出和检索 LangBot 实例中的所有知识库,不受流水线配置限制。
### 列出所有知识库
@@ -486,7 +447,7 @@ for entry in results:
```
-这些 API 不需要 `query_id`,可以在任何组件中使用(包括 Tool 组件),且可以访问所有知识库,不受当前流水线配置限制。
+这些 API 不需要 `query_id`,可以在普通组件中使用(包括 Tool 组件),且可以访问所有知识库,不受当前流水线配置限制。
## RAG API
@@ -676,3 +637,95 @@ async def get_knowledge_file_stream(self, storage_path: str) -> bytes:
# 调用示例
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
```
+
+
+## 其他资源 API
+
+以下方法同样通过 `self.plugin` 调用。
+
+### 调用重排模型
+
+```python
+async def invoke_rerank(
+ self, rerank_model_uuid: str, query: str, documents: list[str],
+ top_k: int | None = None, extra_args: dict[str, Any] | None = None,
+ timeout: float = 60.0,
+) -> list[dict[str, Any]]:
+ ...
+
+scores = await self.plugin.invoke_rerank(
+ rerank_model_uuid=rerank_model_uuid,
+ query="How do I configure a bot?",
+ documents=["Bot configuration guide", "Plugin publishing guide"],
+ top_k=1,
+)
+```
+
+返回结果通常包含原文档索引 `index` 和相关性分数 `relevance_score`。
+
+### 调用解析器
+
+```python
+async def invoke_parser(
+ self, plugin_author: str, plugin_name: str, storage_path: str,
+ mime_type: str, filename: str, metadata: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ ...
+
+parsers = await self.plugin.list_parsers(mime_type="application/pdf")
+if parsers:
+ parser = parsers[0]
+ parsed = await self.plugin.invoke_parser(
+ plugin_author=parser["plugin_author"],
+ plugin_name=parser["plugin_name"],
+ storage_path=storage_path,
+ mime_type="application/pdf",
+ filename="guide.pdf",
+ )
+```
+
+`storage_path` 是宿主存储中的文件路径,例如知识库摄取上下文的 `context.file_object.storage_path`,不是插件本地路径。返回字典包含 `text`、`sections`、`metadata`。
+
+### 列出向量记录
+
+```python
+async def vector_list(
+ self, collection_id: str, filters: dict[str, Any] | None = None,
+ limit: int = 20, offset: int = 0,
+) -> dict[str, Any]:
+ ...
+
+page = await self.plugin.vector_list(
+ collection_id=collection_id,
+ filters={"file_id": "doc_001"},
+ limit=20,
+ offset=0,
+)
+for item in page["items"]:
+ print(item["id"], item.get("metadata"))
+```
+
+返回 `items` 和 `total`。条目包含 `id`、`document`、`metadata`;`total` 是存储后端尽力提供的匹配数量。过滤条件使用上文的向量 API 格式。
+
+### 列出插件和命令
+
+```python
+manifests = await self.plugin.list_plugins_manifest()
+commands = await self.plugin.list_commands()
+```
+
+两个方法均不需要参数,分别返回宿主提供的插件清单列表和命令列表。
+
+### Box 沙箱
+
+这些方法通过 `self.plugin` 调用,作用于当前工作区。在运行器内调用时会自动携带本次运行的授权。
+
+| 方法 | 返回值 | 说明 |
+| --- | --- | --- |
+| `await self.plugin.get_box_status()` | `BoxStatus` | `enabled`、`available`、数量上限 `limit`、已用 `used`、剩余 `remaining`、托管环境要求的 `required_reuse_key` 和不可用原因 `reason` |
+| `await self.plugin.list_boxes()` | `list[BoxSession]` | 可用 Box 的 `id` 和状态快照 `status`(`idle`、`running`、`closing`) |
+| `await self.plugin.acquire_box(reuse_key, options=None)` | `BoxSession` | 同一键复用已有 Box,否则在数量限制内创建;`options` 支持 `image` |
+
+数量未知时对应字段为 `None`。`remaining=0` 仍可以复用已有 Box;创建时由 Runtime 原子检查额度。运行器自行计算复用键,Host 不解释聊天模板;托管环境给出 `required_reuse_key` 时须使用该值。不同工作区的相同键不会共用 Box。
+
+获取 Box 后,通过[运行器上下文](/zh/plugin/dev/apis/agent-run#runner)绑定到本次运行,再导入附件和执行工具。
diff --git a/zh/plugin/dev/apis/messages.mdx b/zh/plugin/dev/apis/messages.mdx
index a5a5cee67..5bce63754 100644
--- a/zh/plugin/dev/apis/messages.mdx
+++ b/zh/plugin/dev/apis/messages.mdx
@@ -58,35 +58,8 @@ msg_chain = MessageChain([
## 其他实体
-除了上述实体之外,还有`好友 Friend`和`群 Group`等实体,这些实体的定义位于`langbot_plugin.api.entities.platform.entities`中。
+除了上述实体之外,还有`好友 Friend`和`群 Group`等实体,这些实体的定义位于`langbot_plugin.api.entities.builtin.platform.entities`中。
-## 访问消息平台底层 API
+## 平台 API
-
- 4.x 暂不适用。
-
-
-为了抹平各个平台的差异,LangBot 在消息平台API之上,提供了一个抽象层,即上方所述的内容,以及`pkg/platform/sources`目录下的各个平台适配器。\
-但由于各个平台的差异较大,抽象层内的实体和 API 封装并不完整,如果您的插件需要访问特定平台的功能,可以按照以下说明访问底层 API。
-
-
- ```python
- # 例如在插件中
- @handler(GroupMessageReceived)
- async def _(self, ctx: EventContext):
- # 取得适配器对象
- adapter = ctx.query.adapter
- ```
-
-
-| 平台 | 适配器 | 访问方式 | 说明 |
-| ---------- | --------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| OneBot v11 | aiocqhttp | adapter.bot | bot 对象对应 [aiocqhttp](https://github.com/nonebot/aiocqhttp) 的 CQHttp 对象,详细使用方式请参考 aiocqhttp 文档 |
-| QQ 官方API | qqofficial | adapter.bot | bot 对象为 libs/qq_official_api 下的 SDK 对象,详细使用方式可以参考 libs/qq_official_api/api.py 中`QQOfficialClient`中的`send_group_text_msg`方法发起 HTTP 请求的方式,具体接口文档请查看 QQ 官方 API 文档 |
-| 企业微信 | wecom | adapter.bot | 参考 libs/wecom_api/api.py 中`WecomClient`中的`send_image`方法发起 HTTP 请求的方式,具体接口文档请查看企业微信 API 文档 |
-| 微信公众号 | officialaccount | - | 请参考微信公众号的 API 文档中的接口文档信息,相关的凭据信息可以在 adapter.bot 中取得 |
-| 飞书 | lark | adapter.api_client | 飞书 SDK 的 API Client 对象,具体请参考[oapi-sdk-python](https://github.com/larksuite/oapi-sdk-python) |
-| 钉钉 | dingtalk | - | 请参考钉钉 API 文档,相关的凭据信息可以在 adapter.bot 中取得 |
-| Discord | discord | adapter.bot | bot 对象为 [Rapptz/discord.py](https://github.com/Rapptz/discord.py) 的 Discord 对象,详细使用方式请参考文档 |
-| Telegram | telegram | adapter.bot | bot 对象为 [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) 的 Telegram 对象,详细使用方式请参考文档 |
-| Slack | slack | adapter.bot | bot 对象 libs/slack_api/api.py 中的对象,详细使用方式请参考其代码以及 Slack SDK |
\ No newline at end of file
+查询用户、管理群组和调用平台专有接口,请使用[平台 API](/zh/plugin/dev/apis/platform)。运行器使用相同的 `self.plugin` 入口,当前会话回复使用 `ctx.reply()`。
diff --git a/zh/plugin/dev/apis/pipeline-events.mdx b/zh/plugin/dev/apis/pipeline-events.mdx
index 0b3dcd08c..89903dfc1 100644
--- a/zh/plugin/dev/apis/pipeline-events.mdx
+++ b/zh/plugin/dev/apis/pipeline-events.mdx
@@ -259,42 +259,13 @@ class PromptPreProcessing(BaseEventModel):
"""此对话现有消息记录,可修改,langbot_plugin.api.entities.builtin.provider.message.Message 或 langbot_plugin.api.entities.builtin.provider.message.MessageChunk 类型"""
```
-## 事件上下文 API
+## EventListener 上下文
-```python
-...
- @self.handler(events.PersonMessageReceived)
- async def handler(event_context: context.EventContext):
- ...
-```
-
-事件处理方法将被传入`EventContext`对象,该对象包含事件的上下文信息,该对象同时具有[请求 API](/zh/plugin/dev/apis/common)和事件上下文特定 API,以下为事件上下文特定 API 列表:
-
-### 获取原事件属性
-
-`event_context.event` 属性为原事件对象,可获取原事件的属性。其类型即为监听的事件类型。
-
-例如:当监听的事件是`GroupMessageReceived`时:
-
-```python
-event = event_context.event
-print(event.launcher_type) # 发起对象类型(group)
-print(event.launcher_id) # 群号
-print(event.sender_id) # 发送者账号 ID
-print(event.message_chain) # 消息链
-```
+事件处理函数通过 `event_context.event` 读取当前事件。`EventContext` 的完整属性和方法见[上下文 API](/zh/plugin/dev/apis/agent-run#eventlistener)。
### 阻止默认行为
-```python
-def prevent_default(self):
- """阻止默认行为"""
-
-# 调用示例
-event_context.prevent_default()
-```
-
-调用该方法后,该事件后续默认行为将被阻止,流水线将直接结束。
+调用 `event_context.prevent_default()` 后,当前事件的默认处理将停止。仅以下事件支持此操作:
仅以下事件可阻止默认行为:
@@ -308,14 +279,4 @@ event_context.prevent_default()
- NormalMessageResponded
-### 阻止后序插件执行
-
-```python
-def prevent_postorder(self):
- """阻止后序插件执行"""
-
-# 调用示例
-event_context.prevent_postorder()
-```
-
-调用该方法后,注册了该事件的后序插件此次将不被执行。
\ No newline at end of file
+调用 `event_context.prevent_postorder()` 可阻止后续插件继续处理当前事件。
diff --git a/zh/plugin/dev/apis/platform.mdx b/zh/plugin/dev/apis/platform.mdx
new file mode 100644
index 000000000..731c38507
--- /dev/null
+++ b/zh/plugin/dev/apis/platform.mdx
@@ -0,0 +1,78 @@
+---
+title: "平台 API"
+---
+
+通过机器人 UUID 调用平台适配器的 API。发送消息链可继续使用 `self.plugin.send_message()`;查询群组、用户及其他平台能力使用下面的接口。
+
+## 调用平台 API
+
+```python
+async def call_platform_api(
+ self,
+ bot_uuid: str,
+ action: str,
+ params: dict[str, Any] | None = None,
+) -> Any:
+ ...
+```
+
+在 Command、EventListener、Tool 等组件中通过 `self.plugin` 调用。返回值为可序列化的字典、列表、标量或 `None`,具体取决于动作;不是平台 SDK 对象。
+
+```python
+group = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="get_group_info",
+ params={"group_id": "123456"},
+)
+```
+
+`bot_uuid` 可从 `await self.plugin.get_bots()` 获取,流水线事件中也可使用 `await event_context.get_bot_uuid()`。平台 ID 的值应来自实际事件或平台配置。
+
+### 常用动作
+
+| `action` | `params` |
+| --- | --- |
+| `get_user_info` | `user_id` |
+| `get_friend_list` | `{}` |
+| `get_group_info` | `group_id` |
+| `get_group_list` | `{}` |
+| `get_group_member_list` | `group_id` |
+| `get_group_member_info` | `group_id`, `user_id` |
+| `get_message` / `delete_message` | `chat_type`, `chat_id`, `message_id` |
+| `set_group_name` | `group_id`, `name` |
+| `mute_member` | `group_id`, `user_id`, `duration` |
+| `unmute_member` / `kick_member` | `group_id`, `user_id` |
+| `leave_group` | `group_id` |
+| `approve_friend_request` | `request_id`, `approve`, `remark` |
+| `approve_group_invite` | `request_id`, `approve` |
+| `get_file_url` | `file_id` |
+
+
+每个平台支持的动作不同,也可能要求机器人管理员权限。`duration` 单位为秒,`0` 的含义依平台而异,不应作为通用的永久禁言值。查询结果可能依赖平台缓存。机器人未运行、动作不受支持或参数不正确时,调用会抛出异常。
+
+## 平台专有 API
+
+将外层 `action` 设为 `call_platform_api`,在 `params` 中填写平台动作及其参数。例如,使用 OneBot Omni 读取机器人账号信息:
+
+```python
+account = await self.plugin.call_platform_api(
+ bot_uuid=bot_uuid,
+ action="call_platform_api",
+ params={"action": "get_login_info", "params": {}},
+)
+```
+
+只能调用适配器已声明的动作,并非任意平台接口都能透传。插件通过运行时调用宿主,不直接访问 `ctx.query.adapter` 或 `adapter.bot`。
+
+## 运行器
+
+运行器执行期间,`self.plugin` 的模型、工具、知识库、存储和平台调用自动关联当前运行并执行授权检查,无需传入 `run_id`。同一插件的并发运行互相隔离。不要在处理函数返回后继续调用这些运行关联的 API。
+
+通过 `await ctx.get_bot_uuid()` 获取当前机器人。平台调用必须匹配本次工具授权:仅授权当前事件目标时,不能改传其他目标;未授权的动作(包括平台专有动作)会被拒绝。调试中的这些调用同样使用 Mock。
+
+```python
+tools = await ctx.get_available_tools()
+if any(tool["name"] == "event_get_actor" for tool in tools):
+ actor = await ctx.call_tool("event_get_actor")
+ await ctx.log(str(actor))
+```
diff --git a/zh/plugin/dev/basic-info.mdx b/zh/plugin/dev/basic-info.mdx
index 8ffc5f20e..1833bf85f 100644
--- a/zh/plugin/dev/basic-info.mdx
+++ b/zh/plugin/dev/basic-info.mdx
@@ -159,6 +159,7 @@ spec:
```yaml
- name: mode
type: select
+ allow_custom: true
options: # 下拉框选项,支持多语言。
- name: mode1 # 值,在下拉框中以小字显示。
label: # 显示名称,支持多语言。
@@ -171,6 +172,8 @@ spec:
...
```
+设置 `allow_custom: true` 可在预设之外提供“自定义”选项,仅选择该项时显示输入框。最终保存的是所选 `name` 或自定义字符串,不会保存“自定义”标记。此选项也适用于运行器的 `spec.config`。
+
### type: prompt-editor
提示词编辑器。会展示一个流水线配置页面的提示词编辑器,最终结果表示为 OpenAI 的 `messages` 格式。
@@ -305,4 +308,4 @@ for tool_name in selected_tools:
该教程将指引您逐步完善插件功能。
-- 添加组件:插件组件是插件的核心功能单元,您可以根据需求[添加组件](/zh/plugin/dev/components/add)。
\ No newline at end of file
+- 添加组件:插件组件是插件的核心功能单元,您可以根据需求[添加组件](/zh/plugin/dev/components/add)。
diff --git a/zh/plugin/dev/components/add.mdx b/zh/plugin/dev/components/add.mdx
index 3ad704041..cc1345b3b 100644
--- a/zh/plugin/dev/components/add.mdx
+++ b/zh/plugin/dev/components/add.mdx
@@ -4,6 +4,8 @@ title: "添加组件"
插件由一种或多种组件构成,为 LangBot 提供不同的功能。目前支持的组件类型有:
+- 插件处理器(运行器):处理机器人收到的消息、成员入群等事件。详见[开发教程](/zh/plugin/dev/components/runner)。
+- Agent 使用的运行器:为 Agent 提供模型调用、外部 Agent 服务连接等运行方式。
- 事件监听器(EventListener):监听流水线执行期间的事件,对上下文或流水线进行修改。
- 命令(Command):由用户通过`!`(或其他已设置的前缀)开头的命令消息触发。
- 工具(Tool):供 LangBot 内置的 Local Agent 在执行期间由 LLM 调用。
@@ -71,6 +73,7 @@ components:
各个组件的详细使用方式请参考:
+- [运行器](./runner)
- [EventListener](./event-listener)
- [Command](./command)
- [Tool](./tool)
diff --git a/zh/plugin/dev/components/command.mdx b/zh/plugin/dev/components/command.mdx
index 816d03c67..7857c7a32 100644
--- a/zh/plugin/dev/components/command.mdx
+++ b/zh/plugin/dev/components/command.mdx
@@ -211,4 +211,5 @@ yield CommandReturn(
您已经了解了命令注册和命令执行的基本信息,接下来可以:
-- 查看[插件通用 API](/zh/plugin/dev/apis/common)
+- 查看完整的 [`ExecuteContext` 上下文 API](/zh/plugin/dev/apis/agent-run#command)
+- 查看 [LangBot API](/zh/plugin/dev/apis/common)
diff --git a/zh/plugin/dev/components/event-listener.mdx b/zh/plugin/dev/components/event-listener.mdx
index 4bd61cec2..5e1847348 100644
--- a/zh/plugin/dev/components/event-listener.mdx
+++ b/zh/plugin/dev/components/event-listener.mdx
@@ -2,6 +2,10 @@
title: "组件:事件监听器"
---
+
+本组件用于扩展流水线的处理流程。如果您需要处理机器人收到的消息、成员入群等事件,请参阅[事件处理器](/zh/plugin/dev/components/runner)。
+
+
LangBot 的流水线在运行期间会产生一些事件,供插件 Hook 并处理。每个插件中只允许添加一个事件监听器(components/event_listener/default.yaml),但在其中可以注册任意数量的事件。
## 添加事件监听器组件
@@ -99,5 +103,6 @@ class EventContext(pydantic.BaseModel):
您已经了解了事件监听器的基本信息,接下来可以:
-- 在文档[流水线事件](/zh/plugin/dev/apis/pipeline-events)中查看`可监听的事件列表`和`事件上下文 API`
-- 查看[插件通用 API](/zh/plugin/dev/apis/common)
+- 在[流水线事件](/zh/plugin/dev/apis/pipeline-events)中查看可监听的事件及字段
+- 查看完整的 [`EventContext` 上下文 API](/zh/plugin/dev/apis/agent-run#eventlistener)
+- 查看 [LangBot API](/zh/plugin/dev/apis/common)
diff --git a/zh/plugin/dev/components/runner.mdx b/zh/plugin/dev/components/runner.mdx
new file mode 100644
index 000000000..f94a824ad
--- /dev/null
+++ b/zh/plugin/dev/components/runner.mdx
@@ -0,0 +1,199 @@
+---
+title: "组件:运行器"
+---
+
+运行器组件通过 SDK 的 `Runner` 类型实现 Agent 的运行逻辑,也可以按事件注册处理函数,供「插件处理器」使用。两种写法共用 `RunnerContext`,插件安装后由用户选择组件并配置。
+
+环境准备见[插件开发教程](/zh/plugin/dev/tutor)。
+
+`RunnerContext` 的完整字段和方法见[上下文 API](/zh/plugin/dev/apis/agent-run#runner)。
+
+## 添加运行器组件
+
+单个插件中可以添加多个事件处理器。请在插件目录执行命令 `lbp comp Runner`,名称填写 `welcome`,描述填写 `Welcome new members`。
+
+```bash
+lbp comp Runner
+```
+
+生成的 `components/runner/welcome.yaml` 定义组件信息,`welcome.py` 是处理程序。
+
+## 自定义运行逻辑
+
+需要接入模型或实现自己的执行循环时,声明 `spec.usages: [agent]`,在 `run(ctx)` 中返回结果:
+
+```python
+from langbot_plugin.api.definition.components.runner import Runner, RunnerContext, RunnerResult
+from langbot_plugin.api.entities.builtin.provider.message import Message
+
+
+class Echo(Runner):
+ async def run(self, ctx: RunnerContext):
+ yield RunnerResult.run_completed(
+ ctx.run_id,
+ message=Message(role="assistant", content=ctx.input.to_text()),
+ )
+```
+
+## 清单文件:运行器
+
+将 `welcome.yaml` 修改为:
+
+```yaml
+apiVersion: langbot/v1
+kind: Runner
+metadata:
+ name: welcome
+ label:
+ en_US: Welcome members
+ zh_Hans: 欢迎新成员
+ ja_JP: 新しいメンバーを歓迎
+ description:
+ en_US: Welcome new members and respond to /hello.
+ zh_Hans: 欢迎新成员,并响应 /hello 消息。
+ ja_JP: 新しいメンバーを歓迎し、/hello に返信します。
+spec:
+ usages: [event]
+ events:
+ - group.member_joined
+ - message.received
+ capabilities:
+ tool_calling: true
+ permissions:
+ tools: [detail, call]
+ config:
+ - name: greeting
+ type: string
+ label:
+ en_US: Greeting
+ zh_Hans: 欢迎语
+ ja_JP: 歓迎メッセージ
+ required: true
+ default: Welcome aboard!
+ - name: reply_enabled
+ type: boolean
+ label:
+ en_US: Send replies
+ zh_Hans: 发送回复
+ ja_JP: 返信する
+ default: true
+execution:
+ python:
+ path: ./welcome.py
+ attr: Welcome
+```
+
+`spec.events` 指定支持的事件,`spec.config` 定义用户可在处理器详情页修改的配置。本例支持成员入群和收到消息,并提供欢迎语、回复开关两个配置项。回复消息需要上面的 `tool_calling` 和 `permissions.tools` 配置。
+
+
+`spec.usages` 必填、无默认值,决定组件可在哪类处理器中选择:`agent` 用于 Agent 和流水线,`event` 用于插件处理器。可同时声明 `[agent, event]`;包含 `event` 时必须填写 `spec.events`。本例使用事件处理函数,也可以在 `run(ctx)` 中按需调用 `await super().run(ctx)`。
+
+## 插件处理
+
+在 `Welcome` 类的 `initialize` 方法中注册事件处理函数。将 `welcome.py` 修改为:
+
+```python
+from langbot_plugin.api.definition.components.runner import (
+ Runner,
+ RunnerContext,
+)
+from langbot_plugin.api.entities.builtin.platform.events import (
+ MemberJoinedEvent,
+ MessageReceivedEvent,
+)
+from langbot_plugin.api.entities.builtin.platform.message import Plain
+
+
+class Welcome(Runner):
+ async def initialize(self):
+ await super().initialize()
+
+ @self.handler(MemberJoinedEvent)
+ async def on_join(ctx: RunnerContext):
+ name = ctx.platform_event.member.nickname or str(ctx.platform_event.member.id)
+ await ctx.log(f"Member joined: {name}")
+ if not ctx.config.get("reply_enabled", True):
+ await ctx.log("Replies are disabled for this processor")
+ return
+ greeting = ctx.config.get("greeting", "Welcome aboard!")
+ await ctx.reply(f"{name}, {greeting}")
+ await ctx.log("Welcome action completed")
+
+ @self.handler(MessageReceivedEvent)
+ async def on_message(ctx: RunnerContext):
+ text = "".join(
+ item.text for item in ctx.platform_event.message_chain if isinstance(item, Plain)
+ ).strip()
+ if text != "/hello":
+ await ctx.log("Message ignored: expected /hello")
+ return
+ if ctx.config.get("reply_enabled", True):
+ await ctx.reply(ctx.config.get("greeting", "Welcome aboard!"))
+ else:
+ await ctx.log("Replies are disabled for this processor")
+```
+
+`on_join` 在成员入群时发送欢迎语,`on_message` 回复 `/hello`。关闭「发送回复」后,只记录日志。
+
+处理函数中,`ctx.platform_event` 保存当前事件的数据,`ctx.config` 是当前处理器的配置。调用 `ctx.reply()` 回复消息,`ctx.log()` 记录日志。函数执行完毕,本次处理即结束。
+
+## 流式回复
+
+使用 `ctx.reply_stream()` 更新同一条回复,`update(text)` 接收当前的完整文本:
+
+```python
+async with ctx.reply_stream() as reply:
+ await reply.update("正在处理…")
+ await reply.update("处理完成。")
+```
+
+正常退出 `async with` 时完成回复。平台不支持流式、未开启流式回复或当前不是消息事件时,会在结束后发送一条完整消息;发生异常时不发送缓存中的未完成文本。调试模式仅模拟发送。
+
+
+## 事件注册
+
+通过 `@self.handler(事件类)` 注册处理函数,并将对应事件标识加入清单的 `spec.events`。例如,`MemberJoinedEvent` 对应 `group.member_joined`。
+
+| 事件 | 清单中的标识 | SDK 事件类 |
+| --- | --- | --- |
+| 收到消息 | `message.received` | `MessageReceivedEvent` |
+| 消息被编辑 | `message.edited` | `MessageEditedEvent` |
+| 消息被删除 | `message.deleted` | `MessageDeletedEvent` |
+| 消息表情回应 | `message.reaction` | `MessageReactionEvent` |
+| 收到反馈 | `feedback.received` | `FeedbackReceivedEvent` |
+| 成员加入群组 | `group.member_joined` | `MemberJoinedEvent` |
+| 成员离开群组 | `group.member_left` | `MemberLeftEvent` |
+| 成员被禁言 | `group.member_banned` | `MemberBannedEvent` |
+| 群组信息更新 | `group.info_updated` | `GroupInfoUpdatedEvent` |
+| 收到好友申请 | `friend.request_received` | `FriendRequestReceivedEvent` |
+| 添加好友 | `friend.added` | `FriendAddedEvent` |
+| 删除好友 | `friend.removed` | `FriendRemovedEvent` |
+| 机器人被邀请入群 | `bot.invited_to_group` | `BotInvitedToGroupEvent` |
+| 机器人被移出群组 | `bot.removed_from_group` | `BotRemovedFromGroupEvent` |
+| 机器人被禁言 | `bot.muted` | `BotMutedEvent` |
+| 机器人被解除禁言 | `bot.unmuted` | `BotUnmutedEvent` |
+| 平台自定义事件 | `platform.specific` | `PlatformSpecificEvent` |
+
+可用事件及字段见 SDK 的 [events.py](https://github.com/langbot-app/langbot-plugin-sdk/blob/main/src/langbot_plugin/api/entities/builtin/platform/events.py)。各平台支持的事件有所不同。
+
+## 测试事件处理器
+
+按[插件开发教程](/zh/plugin/dev/tutor)配置调试连接,在插件目录执行 `lbp run`。然后在 LangBot 中:
+
+1. 创建「插件处理器」,在详情页选择「欢迎新成员」组件,填写配置并保存。
+2. 在左侧选择「成员加入群组」,填写成员昵称、ID 和群组 ID,点击「运行测试」。
+3. 查看回复和日志。也可以选择「收到消息」,输入 `/hello` 测试回复。
+
+调试中的平台回复使用 Mock,不发送真实消息。修改组件 YAML 后,请重启 `lbp run`。
+
+安装插件后,在机器人详情页的「插件处理器」中添加该配置并保存。也可以直接在这里选择组件、填写配置,创建并绑定新配置。机器人会自动投递组件声明的事件,无需逐条配置事件路由。
+
+插件处理器与 Agent/流水线路由独立执行,同一事件可以触发多个插件处理器,某个处理器失败不会阻止其他处理。使用同一配置的机器人会共享设置和运行状态;需要分别设置时,创建不同的配置。可选功能应通过组件配置提供,例如是否回复消息,避免与其他处理器重复回复。
+
+## 接下来做什么
+
+- 查看 [`RunnerContext` 上下文 API](/zh/plugin/dev/apis/agent-run#runner)。
+- 查看模型、工具、知识库和存储相关的 [LangBot API](/zh/plugin/dev/apis/common)。
+- 查看消息发送等[平台 API](/zh/plugin/dev/apis/platform)。
+- 查看 [RunnerDemo](https://github.com/langbot-app/langbot-plugin-demo/tree/main/Runner/RunnerDemo) 中的更多示例。
+- 如果要扩展流水线的预处理、模型调用完成等步骤,请使用[事件监听器](/zh/plugin/dev/components/event-listener)。
diff --git a/zh/plugin/dev/components/tool.mdx b/zh/plugin/dev/components/tool.mdx
index 6900aae31..3d93be191 100644
--- a/zh/plugin/dev/components/tool.mdx
+++ b/zh/plugin/dev/components/tool.mdx
@@ -174,4 +174,5 @@ httpx
您已经了解了工具的基本用法,接下来可以:
-- 查看[插件通用 API](/zh/plugin/dev/apis/common)
+- 查看 Tool 的 [`session` 和 `query_id` 说明](/zh/plugin/dev/apis/agent-run#tool)
+- 查看 [LangBot API](/zh/plugin/dev/apis/common)
diff --git a/zh/plugin/dev/tutor.mdx b/zh/plugin/dev/tutor.mdx
index 82d40245e..089977061 100644
--- a/zh/plugin/dev/tutor.mdx
+++ b/zh/plugin/dev/tutor.mdx
@@ -2,6 +2,10 @@
title: "插件开发教程"
---
+
+要开发处理消息、成员入群等事件的插件,可在完成基础环境准备后,继续阅读[事件处理器教程](/zh/plugin/dev/components/runner)。
+
+
## 架构基础
在 4.0 版本中,我们引入了一种高安全性、高灵活性的生产级插件系统,并为插件开发者提供了丰富的 API 和易用的配套工具。
diff --git a/zh/usage/mcp/readme.mdx b/zh/usage/mcp/readme.mdx
index 2b5beb89d..fed64e4fb 100644
--- a/zh/usage/mcp/readme.mdx
+++ b/zh/usage/mcp/readme.mdx
@@ -23,7 +23,7 @@ LangBot 支持调用来自 MCP 服务器的工具。添加 MCP 后,启用了
-MCP 工具需要流水线使用**内置 Agent**,并选择**已开启工具使用能力**的模型——见[模型](../models/readme)。Dify、n8n、Langflow、Coze 等外部 Runner 使用各自平台的工具机制。
+MCP 工具需要流水线使用**内置 Agent**,并选择**已开启工具使用能力**的模型——见[模型](../models/readme)。Dify、n8n、Langflow、Coze 等外部运行器使用各自平台的工具机制。
## 安装 MCP 服务器
diff --git a/zh/usage/pipelines/tbox.mdx b/zh/usage/pipelines/tbox.mdx
index 244ac0372..96e1a7e32 100644
--- a/zh/usage/pipelines/tbox.mdx
+++ b/zh/usage/pipelines/tbox.mdx
@@ -5,7 +5,7 @@ description: "在 LangBot 中使用 TBox,为 AI 机器人对话管线提供支
[TBox(百宝箱)](https://tbox.cn/)是蚂蚁集团推出的面向 AI 开发者的一站式智能体应用开发平台。在平台上,无论您是否拥有编程基础,都可以通过自然语言,基于各种大模型搭建属于您自己的智能体应用,并将其发布到支付宝小程序、web 服务、浏览器插件等生态渠道。
-LangBot 支持接入 TBox 上编排好的智能体,用作流水线 Runner。
+LangBot 支持接入 TBox 上编排好的智能体,用作流水线运行器。
在 TBox 上搭建好智能体后,请到[`开放平台`](https://www.tbox.cn/open/authorized-management)创建 API Key:
diff --git a/zh/usage/sandbox/readme.mdx b/zh/usage/sandbox/readme.mdx
index 089941c91..18b368100 100644
--- a/zh/usage/sandbox/readme.mdx
+++ b/zh/usage/sandbox/readme.mdx
@@ -21,7 +21,7 @@ Box Runtime 给内置 Agent 一个统一的执行环境:跑命令、读写文
`exec` 在所选 Box 后端中运行;其余五个直接读写映射到 `/workspace` 的工作区目录。选择 Host 时,命令也直接运行在 Box Runtime 主机上。
-这些工具面向内置 Agent。使用 Dify、n8n、Langflow、Coze 等外部 Runner 时,请使用对应平台自己的工具机制。
+这些工具面向内置 Agent。使用 Dify、n8n、Langflow、Coze 等外部运行器时,请使用对应平台自己的工具机制。
## 沙箱作用域
diff --git a/zh/usage/sandbox/runtime-relationships.mdx b/zh/usage/sandbox/runtime-relationships.mdx
index 090d11883..ac27da2d4 100644
--- a/zh/usage/sandbox/runtime-relationships.mdx
+++ b/zh/usage/sandbox/runtime-relationships.mdx
@@ -48,5 +48,5 @@ Box API、Agent 工具、会话作用域、生命周期、Skills 和 stdio MCP
| 外部 Host Box 连接被拒绝 | LangBot 与 Box Runtime 两侧设置相同的 `LANGBOT_BOX_CONTROL_TOKEN` |
-沙箱内置工具和 Skills 当前面向内置 Agent。外部 Runner(Dify、n8n、Langflow、Coze 等)应使用各自平台的工具和执行机制。
+沙箱内置工具和 Skills 当前面向内置 Agent。外部运行器(Dify、n8n、Langflow、Coze 等)应使用各自平台的工具和执行机制。
diff --git a/zh/usage/skills/readme.mdx b/zh/usage/skills/readme.mdx
index 1e68b2a1b..f1324a604 100644
--- a/zh/usage/skills/readme.mdx
+++ b/zh/usage/skills/readme.mdx
@@ -97,5 +97,5 @@ LangBot 当前读取以下三个字段:
| `register_skill` 报错 | 路径必须在 `/workspace` 下,目录或两层内子目录需含 `SKILL.md` |
-Skills 当前面向内置 Agent,依赖函数调用和沙箱后端。外部 Runner(Dify、n8n、Langflow、Coze 等)请使用各自平台的等价机制。
+Skills 当前面向内置 Agent,依赖函数调用和沙箱后端。外部运行器(Dify、n8n、Langflow、Coze 等)请使用各自平台的等价机制。