agentscope-ai / agentscope-ai/QwenPaw

[Feature]: Plugin API extensions required by DataPaw

Đang mở
#4,749 1 bình luận 0 reaction 2 người được giao Được @qbc2016 nhận Xem trên GitHub
enhancement
Ngôn ngữ chính
Python
Star
34.9k
Fork
3.1k
Merge trung bình
1 ngày 15 giờ
Pull request đã merge (30 ngày)
225

Mô tả

**Title (EN)**: `[Feature]: Plugin API extensions required by DataPaw`

**标题(中文)**: `[Feature]: DataPaw 插件所需的宿主 API 扩展`

> English version first, 中文版在文末(同一份内容的两种语言)

---

# English

## Summary

DataPaw is QwenPaw's data-analysis plugin (sub-mode). To work today it relies on a smart agent factory, a monkey-patched `PluginLoader.unload_plugin`, a SSE-frame-rewriting channel wrapper, four ReAct/prompt method overrides, and `sys.path.insert` import workarounds — all because the host does not yet expose the right extension points. This issue requests 7 plugin API additions + 1 host shell-tool bugfix (T1, T2, T3, T5, T7, T9, T11, T13), plus tracks 5 follow-up plugin-side cleanups (T4, T6, T8, T10, T12) that DataPaw will land in the same PR cycles to fully remove its current workarounds.

## Component(s) Affected

- [x] Core / Backend (app, agents, config, providers, utils, local_models) — plugin API surface, agent base class, Msg/Event schema + Msg→Event converter, prompt assembly, plugin loader / validator, ReAct lifecycle, shell tool
- [ ] Console (frontend web UI)
- [ ] Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
- [x] Skills — T11 + T12 only (typed `register_skill_provider`)
- [ ] CLI
- [ ] Documentation (website)
- [ ] Tests
- [ ] CI/CD
- [ ] Scripts / Deploy

## Problem / Motivation

DataPaw currently keeps itself working through five invasive workarounds against host private internals:

1. **Smart agent factory** that rewrites the top-level `QwenPawAgent` name in the runner module so DataPaw's subclass gets constructed instead.
2. **Monkey-patched `PluginLoader.unload_plugin`** so DataPaw can clean up its agent profile / `agent.json` / workspace skills on uninstall.
3. **`ConsoleChannel.stream_one` wrapper** that JSON-decodes every SSE frame, caches `metadata` from `message` frames by `msg_id`, and re-injects it into each `content` delta frame — purely because the host event schema has no `metadata` field and the Msg→Event converter does not propagate it.
4. **Four agent method overrides** (`_build_sys_prompt`, `_reasoning`, `_acting`, `_summarizing`) whose bodies are essentially one line of side effect each.
5. **`sys.path.insert` + `if __package__ ... else ...` dual-branch import** in two files to work around the plugin validator's relative-import failure.

Each one blocks multi-plugin coexistence — every workaround assumes DataPaw is the only plugin wrapping that symbol — and forces DataPaw to track host private signatures, naming, and construction order. New host releases can silently break the plugin.

## Proposed Solution

Add 7 plugin API extensions + 1 host shell-tool bugfix. DataPaw will deliver the matching plugin-side cleanup (T4 / T6 / T8 / T10 / T12) in the same PR cycle as each host change, so every host-side TODO lands as a paired set with full workaround removal.

### Task list

**Host-side additions / fixes (independently landable):**

- [ ] **T1** Add a `metadata` field to the host SSE event / message schema and make sure the Msg→Event converter propagates `msg.metadata` to **every** derived event
- [ ] **T2** Provide an outbound metadata enricher hook so plugins can attach routing info such as `graph_id` / `node_id` onto `Msg`
- [ ] **T3** Provide a prompt section hook so plugins can inject prompt fragments (master prompt, mode prompt, planner rules, environment hints, etc.)
- [ ] **T5** Provide `register_uninstall_hook` to replace the `PluginLoader.unload_plugin` monkey-patch
- [ ] **T7** Fix the plugin validator's relative-import handling / `submodule_search_locations`
- [ ] **T9** Provide ReAct lifecycle hooks so plugins can observe reason / act / summarize phases without overriding those methods
- [ ] **T11** Confirm whether the existing skill system already satisfies a typed `register_skill_provider` API
- [ ] **T13** Fix `execute_shell_command` heredoc handling (host shell-tool bug)

**Pairing note**: T2 depends on T1 (and T1 alone delivers no plugin-visible value). They must land together to remove DataPaw's current `print()` override and `ConsoleChannel.stream_one` frame rewrite.

**Plugin-side cleanup (DataPaw delivers in the same PR cycle):**

- [ ] **T4** DataPaw registers prompt sections and removes the `_build_sys_prompt` override (depends on T3)
- [ ] **T6** DataPaw registers an uninstall cleanup hook and removes the `PluginLoader.unload_plugin` monkey-patch (depends on T5)
- [ ] **T8** DataPaw goes back to standard relative imports and removes the `sys.path.insert` / dual-path import workaround (depends on T7)
- [ ] **T10** DataPaw registers a trace-append callback and removes the `_reasoning` / `_acting` / `_summarizing` overrides (depends on T9)
- [ ] **T12** If host exposes a typed skill provider, DataPaw migrates to that API and removes its copytree / manifest patch / mtime cache logic (depends on T11)

### Per-task detail

#### T1 — Host: add `metadata` to Event / Msg schema and propagate it

> **No plugin hook required**. T1 is a pure host-side schema + default-behavior change. The hook for writing values lives in T2.

##### Current problem

A single outbound `Msg` is expanded into multiple wire `Event`s by the upstream Msg→Event converter:

```
Msg(id=M1, metadata={graph_id, node_id})

└─► Event { object="message", id=M1, metadata={graph_id, node_id} }
Event { object="content", msg_id=M1, metadata=∅ } ← problem
Event { object="content", msg_id=M1, metadata=∅ } ← problem
Event { object="content", msg_id=M1, metadata=∅ } ← problem
```

Two things are broken today:

1. `agentscope_runtime.engine.schemas.agent_schemas.Event` does not have a `metadata` field at all. It only exposes `sequence_number / object / status / error`.
2. Even if the field is added, the default Msg→Event converter only carries metadata on the head `message` event. Downstream `content` (delta) events derived from the same Msg do not inherit it.

This is why DataPaw currently needs a `ConsoleChannel.stream_one` wrapper that JSON-decodes every SSE frame, caches `metadata` from the `message` frame by `msg_id`, and re-injects it into each `content` frame.

##### Ask

This is two host-internal changes, neither of which exposes a new plugin API:

**1. Schema** — add a `metadata` field to the event / message schema:

```python
class Event(BaseModel):
sequence_number: int
object: str
status: str
error: str | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
```

**2. Default propagation** — the Msg→Event converter must copy `msg.metadata` onto **every event derived from that Msg**, including all `content` delta events. The propagation is purely mechanical — every plugin needs the same behavior, so it does not warrant a hook:

```python
def msg_to_events(msg: Msg) -> Iterable[Event]:
head = Event(object="message", id=msg.id, metadata=dict(msg.metadata))
yield head
for delta in split_into_content_deltas(msg):
yield Event(
object="content",
msg_id=msg.id,
metadata=dict(msg.metadata), # ← required
...
)
```

The existing serializer needs no change — `model_dump_json()` already outputs all model fields.

##### Outcome

Frontend can read routing info from every chat event (both `message` and `content`):

```ts
const graphId = event.metadata?.graph_id
const nodeId = event.metadata?.node_id
```

Combined with T2, DataPaw deletes its `print()` override and the `ConsoleChannel.stream_one` frame rewrite.

#### T2 — Host: outbound metadata enricher hook

> **Depends on T1**. T1 places the slot; T2 lets plugins fill it. T1 + T2 must land together to remove DataPaw's current workarounds.

##### Current problem

T1 only provides the slot; values still need to be written before the message leaves the agent. Different plugins write different keys (DataPaw writes `graph_id` / `node_id`; another plugin might write something else), so writing the value is per-plugin logic and does need a hook.

DataPaw currently does this without a hook by overriding `agent.print()`:

```python
async def print(self, msg, last=True, speech=None):
if isinstance(msg, Msg):
msg = self._annotate_msg_node_id(msg) # writes msg.metadata
return await super().print(msg, last, speech=speech)
```

Every plugin that wants to attach outbound metadata would have to subclass the agent or monkey-patch `print()` — they would step on each other.

##### Ask

Expose an outbound metadata enricher hook on the plugin API:

```python
api.register_message_metadata_enricher(
name="datapaw_node_tag",
when="outbound",
agent_id="datapaw",
enrich=_datapaw_node_enrich,
)
```

Enricher contract:

```python
def _datapaw_node_enrich(msg, ctx) -> dict:
runtime_state = ctx.agent.plan_notebook # DataPaw's RuntimeStateManager
metadata = {}
if runtime_state.current_graph_id:
metadata["graph_id"] = runtime_state.current_graph_id
if runtime_state.current_node_id:
metadata["node_id"] = runtime_state.current_node_id
return metadata
```

`ctx` must at least expose `agent` (or `session_id`) so the enricher can reach the plugin's own state — the host has no knowledge of DataPaw's graph state and cannot expose it on `ctx` directly.

The host calls all registered enrichers inside `agent.print()` (before `super().print()` / before the Msg is handed to the Msg→Event converter) and merges the returned dict into `msg.metadata`. After that, T1's propagation rule carries the metadata onto every derived Event.

##### Outcome

DataPaw deletes both:

- `DataPawAgent.print()` override and `_annotate_msg_node_id`.
- `ConsoleChannel.stream_one` frame rewrite (`_wrap_stream_one` / `_maybe_inject_node_metadata`).

The plugin only writes `msg.metadata`, never reads or rewrites wire frames.

#### T3 — Host: prompt section registry

##### Current problem

DataPaw's system prompt needs several extra fragments appended after the host base prompt:

- The DataPaw master prompt.
- Mode prompts (agent mode / plan mode).
- Planner rules.
- Analysis environment hints (sandbox config, workspace paths, etc.).

Today this is done by overriding `_build_sys_prompt`. When multiple plugins all contribute fragments to the system prompt, they would have to chain overrides through inheritance order, which does not scale.

##### Ask

Expose a named, ordered prompt section registry:

```python
api.register_prompt_section(
name="datapaw.master",
after="profile",
agent_id="datapaw",
provider=lambda agent: read_master_prompt(agent.lang),
)

api.register_prompt_section(
name="datapaw.env_hint",
after="datapaw.planner",
agent_id="datapaw",
provider=lambda agent: analysis_environment_hint(agent),
)
```

The host names its own core sections (e.g. `agents` / `soul` / `profile` / `env_context`) and topologically sorts by `after=`. The `provider` callback receives either the full agent or a restricted `PromptContext` that at least exposes `mode`, language, sandbox config, and workspace info — DataPaw needs all of these. Conditional inclusion does not need new API: providers can simply return an empty string.

##### Outcome

T4 lands as a follow-up: DataPaw moves all its prompt fragments to `register_prompt_section` and removes `DataPawAgent._build_sys_prompt` entirely.

#### T4 — Plugin: register prompt sections, remove `_build_sys_prompt` override

Depends on T3. Once landed, DataPaw's master prompt, mode prompts, planner rules, and environment hint all move to `register_prompt_section`, and `DataPawAgent._build_sys_prompt` is removed entirely.

#### T5 — Host: `register_uninstall_hook`

##### Current problem

When DataPaw is uninstalled it must clean up the agent profile, `agent.json`, and workspace / skills directories it created. Otherwise the next user sees a dangling agent pointing at a plugin that no longer exists.

Today this is done by monkey-patching `PluginLoader.unload_plugin`:

```python
PluginLoader.unload_plugin = _wrap_unload_plugin(
PluginLoader.unload_plugin,
)
```

This does not scale once multiple plugins all need to wrap the same method — they would have to chain wrappers in unspecified order.

##### Ask

Expose an uninstall hook on the plugin API:

```python
api.register_uninstall_hook(
plugin_id="datapaw",
hook_name="datapaw_cleanup",
callback=uninstall_builtin_agents,
priority=50,
)
```

`PluginLoader.unload_plugin` runs registered hooks in priority order, then runs its own bookkeeping:

```python
async def unload_plugin(plugin_id: str):
for hook in api.uninstall_hooks_for(plugin_id):
await maybe_await(hook.callback())
await _unload_plugin_bookkeeping(plugin_id)
```

`register_uninstall_hook` covers the plugin lifecycle and complements `register_shutdown_hook` (process lifecycle) — they do not replace each other.

##### Outcome

T6 lands as a follow-up: DataPaw deletes its `PluginLoader.unload_plugin` monkey-patch.

#### T6 — Plugin: register the uninstall hook, remove the monkey-patch

Depends on T5. Once landed, DataPaw deletes `PluginLoader.unload_plugin = _wrap_unload_plugin(...)`.

#### T7 — Host: fix plugin validator relative-import handling

##### Current problem

A plugin package's internal modules should be able to use standard relative imports:

```python
from .constants import PLUGIN_DIR
from .agents_setup import ensure_builtin_agents
```

But the installer validator currently loads the plugin entry file with `spec_from_file_location` without passing `submodule_search_locations`, so relative imports inside the plugin package fail at validation time. DataPaw works around this in two places:

```python
sys.path.insert(0, str(PLUGIN_DIR))

if __package__:
from .constants import PLUGIN_DIR
else:
from constants import PLUGIN_DIR
```

With multiple plugins, naturally-named modules like `constants.py` or `utils.py` may collide on `sys.path`.

##### Ask

Both the validator and the runtime loader should load the plugin in package form:

```python
spec = importlib.util.spec_from_file_location(
module_name,
backend_entry_file,
submodule_search_locations=[str(plugin_dir)],
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
```

This is a host loader bugfix, not a new plugin API.

##### Outcome

T8 lands as a follow-up: DataPaw removes the `sys.path.insert` and the dual-path import block.

#### T8 — Plugin: go back to standard relative imports

Depends on T7. Once landed, DataPaw removes `sys.path.insert(0, str(PLUGIN_DIR))` from `constants.py` and the `if __package__ ... else ...` dual-path import block in `plugin.py`.

#### T9 — Host: ReAct lifecycle hooks

##### Current problem

DataPaw currently overrides `_reasoning` / `_acting` / `_summarizing` purely to append the resulting `msg` to the in-progress node's trace:

```python
async def _reasoning(self, *args, **kwargs):
msg = await super()._reasoning(*args, **kwargs)
self.plan_notebook.append_to_trace(msg)
return msg
```

`_acting` / `_summarizing` look identical. Each override is one line of side effect, and overriding three private methods just to get an observation point is fragile: any host signature change forces a plugin update.

##### Ask

Emit a unified lifecycle event before each phase returns, and expose a registration API:

```python
api.register_react_lifecycle_hook(
agent_id="datapaw",
phase="after_reason", # / after_act / after_summarize
callback=_datapaw_append_to_trace,
)
```

Host-side dispatch sketch:

```python
async def _emit_lifecycle(self, phase: str, msg):
callbacks = _lifecycle_registry.get((self.agent_id, phase), [])
ctx = {
"phase": phase,
"session_id": self.session_id,
"request_id": getattr(self, "request_id", None),
}
for cb in callbacks:
try:
result = cb(self, msg, ctx)
if inspect.isawaitable(result):
await result
except Exception:
logger.exception("react lifecycle callback failed")
```

Callback contract: `callback(agent, msg, ctx) -> None | Awaitable[None]`. Callback errors must be isolated and must not affect the main ReAct loop.

##### Outcome

T10 lands as a follow-up: DataPaw deletes the three method overrides.

#### T10 — Plugin: register trace-append callback, remove the three overrides

Depends on T9. Once landed, DataPaw deletes `DataPawAgent._reasoning` / `_acting` / `_summarizing`.

#### T11 — Host: align on typed `register_skill_provider`

##### Current problem

DataPaw ships its own skills and needs to materialize them into the workspace plus write the host skill manifest with `enabled=True`, `source="plugin:datapaw"`, and per-channel defaults. Today the plugin does this by hand:

1. `shutil.copytree` copies plugin skills into the workspace.
2. Calls the host-internal `reconcile_workspace_manifest`.
3. Directly reads / writes `skill.json` and patches `enabled` / `channels` / `source`.
4. Maintains a `.datapaw_versions.json` mtime cache to know what to refresh.

This couples the plugin to the host manifest file structure and field names.

The host `skill_system` already exposes building blocks like `reconcile_workspace_manifest`, `get_workspace_skills_dir`, and `get_builtin_skills_dir`. But the plugin-facing API does not yet have a typed `register_skill_provider`.

##### Ask

Align on whether the existing skill system should be exposed through the plugin API with an explicit provider contract:

```python
api.register_skill_provider(
SkillProvider(
plugin_id="datapaw",
source="plugin:datapaw",
skills_dir=PLUGIN_DIR / "skills",
enabled_by_default=True,
channels=["all"],
)
)
```

The host would then own:

- Copying / mounting the skill directory.
- Reconciling the manifest.
- Applying the default-enabled policy.
- Cleaning up by `source` on uninstall.

##### Outcome

T12 lands as a follow-up once the host position is clear.

#### T12 — Plugin: migrate to typed skill provider

Depends on T11. Once landed, DataPaw removes:

- The `shutil.copytree` that ships plugin skills into the workspace.
- Direct patching of `skill.json` for `enabled` / `channels` / `source`.
- The `.datapaw_versions.json` mtime cache.

#### T13 — Host: fix `execute_shell_command` heredoc handling

##### Current problem

ReAct agents naturally generate heredocs, especially for inline Python:

```bash
python3 << 'PY'
import pandas as pd
print(pd.__version__)
PY
```

DataPaw triggers this constantly during data loading, cleaning, and plotting.

The current `_collapse_newlines_outside_quotes` in `execute_shell_command` collapses the heredoc body into a single line:

```bash
python3 << 'PY' import pandas as pd print(pd.__version__) PY
```

`sh -c` then treats `import` as an argument and the heredoc body is empty, so the command fails 100% of the time.

##### Reproducer (observed with glm-5.1)

The LLM emits this tool call:

```json
{
"command": "cd && python3 << 'PYEOF'\nimport pandas as pd\nimport numpy as np\nimport os\n\n# Load data\ndf = pd.read_csv('media/sales_orders.csv')\n\n# --- Validation ---\nprint(\"=== 基本信息 ===\")\nprint(f\"行数: {df.shape[0]}, 列数: {df.shape[1]}\")\nprint(f\"缺失值统计:\")\nmissing = df.isnull().sum()\nprint(missing[missing > 0] if missing.sum() > 0 else \"无缺失值\")\n\nprint(\"\\n=== 数值列异常检查 ===\")\nnumeric_cols = ['quantity', 'unit_price', 'discount_rate', 'payment_amount', 'cost', 'shipping_fee']\nfor col in numeric_cols:\n negatives = (df[col] < 0).sum()\n if negatives > 0:\n print(f\"{col}: 有 {negatives} 个负值\")\n else:\n print(f\"{col}: 无负值\")\n\n# --- Parse date ---\ndf['order_date'] = pd.to_datetime(df['order_date'])\nprint(f\"\\n日期范围: {df['order_date'].min()} ~ {df['order_date'].max()}\")\n\n# --- Compute derived metrics ---\ndf['gross_profit'] = df['payment_amount'] - df['cost']\ndf['profit_margin'] = df['gross_profit'] / df['payment_amount']\ndf['avg_order_value'] = df['payment_amount'] / df['quantity']\n\n# --- Save cleaned data ---\nsession_id = \"1779948066273\"\ngraph_id = \"graph_Zazottr9\"\nnode_id = \"data_load\"\nout_dir = f\"artifacts/{session_id}/{graph_id}/{node_id}\"\nos.makedirs(out_dir, exist_ok=True)\ndf.to_csv(f\"{out_dir}/sales_orders_cleaned.csv\", index=False)\n\n# --- Summary stats ---\nprint(f\"总销售额(GMV): {df['payment_amount'].sum():.2f}\")\nprint(f\"总订单数: {df.shape[0]}\")\nprint(f\"整体毛利率: {df['gross_profit'].sum() / df['payment_amount'].sum():.4f}\")\nPYEOF"
}
```

Tool result:

```text
[{"type": "text", "text": "Command failed with exit code 2.\n[stderr]\npython3: can't open file '/import': [Errno 2] No such file or directory"}]
```

The error message is the smoking gun: after `_collapse_newlines_outside_quotes` flattens the heredoc body to a single line, `python3` sees `import` (the first token after `<< 'PYEOF'`) as a positional argument and tries to open `/import` as a script file.

This is model-agnostic — any model that emits multi-line heredocs hits the same bug. glm-5.1 is just where it happened to surface.

##### Ask

Extend `_collapse_newlines_outside_quotes` with a heredoc state machine:

1. When it sees `<<` or `<<-`, parse the delimiter.
2. While in heredoc state, preserve all newlines.
3. Exit heredoc state only when a line containing exactly the delimiter appears.

No new API is needed for callers.

##### Outcome

Heredocs work in `execute_shell_command` for every agent, not just DataPaw. This is best tracked as a standalone host shell-tool bug rather than batched with the plugin API work.

### Suggested landing batches

1. **Batch 1 (DAG / Chat stream split)**: T1 + T2. Lets the plugin delete its `ConsoleChannel.stream_one` frame rewrite and `print()` override.
2. **Batch 2 (replace runner / loader monkey-patches)**: T5 + T6, T7 + T8.
3. **Batch 3 (replace inheritance-based overrides)**: T3 + T4, T9 + T10.
4. **Follow-ups / pending alignment**: T11 + T12 (depends on host decision), T13 (independent shell-tool bug).

## Alternatives Considered

- **Keep the existing workarounds**. Works today but does not scale beyond DataPaw: a second plugin needing any of the same extension points would have to chain wrappers around the same private symbols (`QwenPawAgent` name, `PluginLoader.unload_plugin`, `ConsoleChannel.stream_one`, agent `_reasoning` / `_acting` / `_summarizing`) in unspecified order. New host releases can also silently break the monkey-patches.
- **Expose a `register_agent_class` registry instead of per-capability hooks**. Rejected because it forces every plugin that wants to customize one slice of agent behavior (just trace, just prompt, just outbound metadata) to subclass the whole agent and inherit the entire host private surface. The current proposal keeps the host agent class authoritative and lets multiple plugins compose orthogonal hooks.
- **For T1, keep `metadata` only on `message` events and have frontends look it up by `msg_id` against the latest `message` frame**. Rejected because that is exactly what DataPaw's `ConsoleChannel.stream_one` wrapper does today — it requires every consumer of the stream to maintain its own lookup table, and the propagation rule is purely mechanical and identical for every plugin. It belongs in the host converter, not in each consumer.
- **For T2, write metadata directly to the wire `Event` from the channel layer (instead of to `Msg` at the agent boundary)**. Rejected because the enricher would then need to know about every event type derived from one Msg, instead of writing once on the source object and letting T1's propagation carry it forward.
- **For T13, fix the heredoc problem in the plugin (DataPaw-side shell tool)**. Rejected because `execute_shell_command` is a host shell tool used by every ReAct agent, not just DataPaw. Fixing it in the host benefits every downstream user.

## Additional Context

- DataPaw current-implementation pointers (in the DataPaw plugin repo): `plugins/bundle/datapaw/core/agents/base.py` (smart factory, `_build_sys_prompt`, `_reasoning` / `_acting` / `_summarizing`, `print()` override), `plugins/bundle/datapaw/hooks.py` (`PluginLoader.unload_plugin` monkey-patch, `_wrap_stream_one` / `_maybe_inject_node_metadata` SSE frame rewrite), `plugins/bundle/datapaw/constants.py` (`sys.path.insert`), `plugins/bundle/datapaw/plugin.py` (dual-branch import).
- Host-side files referenced above: `src/qwenpaw/app/channels/console/channel.py` (`ConsoleChannel.stream_one`); the upstream Msg / Event schemas live in `agentscope_runtime.engine.schemas.agent_schemas`.
- T13's heredoc bug surfaces during routine DataPaw usage (data loading / cleaning / plotting); the reproducer above was captured during normal session execution.

## Acceptance

For each host TODO that lands, DataPaw will deliver the dependent plugin-side cleanup (T4 / T6 / T8 / T10 / T12) in the same PR cycle and link back to the matching checkbox in this issue from the PR description. The issue is fully resolved when every checkbox above is checked.

## Willing to Contribute

- [x] I am willing to open a PR for this feature (after discussion).

DataPaw maintainers will own the plugin-side cleanup PRs (T4 / T6 / T8 / T10 / T12) in the same PR cycle as the matching host changes. We are also happy to help review host-side PRs (T1 / T2 / T3 / T5 / T7 / T9 / T11 / T13) and to provide concrete usage data from DataPaw where it helps.

---

# 中文

## Summary

DataPaw 是 QwenPaw 的数据分析插件(子模式)。为了在当前 host 上工作,它依赖 smart agent factory、monkey-patch 后的 `PluginLoader.unload_plugin`、对 SSE 帧做 JSON round-trip 的 channel wrapper、四个 ReAct/prompt 方法 override,以及 `sys.path.insert` import workaround —— 全部因为 host 尚未暴露对应扩展点。本 issue 请求 7 个 plugin API 新增 + 1 个 host shell-tool bugfix(T1, T2, T3, T5, T7, T9, T11, T13),并跟踪 5 个 DataPaw 在同 PR 周期内交付的插件侧清理(T4, T6, T8, T10, T12),用于彻底拿掉现有 workaround。

## Component(s) Affected

- [x] Core / Backend (app, agents, config, providers, utils, local_models) —— plugin API surface、agent 基类、Msg/Event schema + Msg→Event 转换器、prompt 拼装、plugin loader / validator、ReAct lifecycle、shell tool
- [ ] Console (frontend web UI)
- [ ] Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
- [x] Skills —— 仅 T11 + T12(typed `register_skill_provider`)
- [ ] CLI
- [ ] Documentation (website)
- [ ] Tests
- [ ] CI/CD
- [ ] Scripts / Deploy

## Problem / Motivation

DataPaw 目前靠五项侵入式 workaround 维持自身运转,全部针对 host 私有 internals:

1. **Smart agent factory**:改写 runner 模块顶层的 `QwenPawAgent` 名字,让原本构造宿主 agent 的代码走 DataPaw subclass。
2. **monkey-patch `PluginLoader.unload_plugin`**:DataPaw 卸载时清理它创建的 agent profile / `agent.json` / workspace skills。
3. **`ConsoleChannel.stream_one` wrapper**:对每个 SSE 帧 JSON decode,按 `msg_id` 缓存 `message` 帧的 `metadata`,再注入到每个 `content` delta 帧 —— 纯粹因为 host event schema 没有 `metadata` 字段,且 Msg→Event 转换器不会把它 propagate 下去。
4. **四个 agent 方法 override**(`_build_sys_prompt`、`_reasoning`、`_acting`、`_summarizing`),每个 override 几乎只有一行 side effect。
5. **两处的 `sys.path.insert` + `if __package__ ... else ...` 双路径 import**,绕过 plugin validator 的相对 import 失败。

每一项都阻碍多插件共存 —— 这些 workaround 都假设 DataPaw 是唯一包同一个符号的插件 —— 并把 DataPaw 绑死在 host 私有签名 / 命名 / 构造顺序上。host 新版本随时可能静默打破插件。

## Proposed Solution

新增 7 个 plugin API 扩展 + 1 个 host shell-tool bugfix。每项 host 变更落地时,DataPaw 在**同一 PR 周期**交付对应的插件侧清理(T4 / T6 / T8 / T10 / T12),保证每对 host + plugin 改动作为一个完整工作单元落地,workaround 彻底拿掉。

### 任务清单

**Host 侧新增 / 修复(独立可落地):**

- [ ] **T1** 给 host SSE event / message schema 增加 `metadata` 字段,并保证 Msg→Event 转换器把 `msg.metadata` propagate 到**每一个**派生 event
- [ ] **T2** 提供 outbound metadata enricher hook,让插件在 `Msg` 上写 `graph_id` / `node_id` 等路由信息
- [ ] **T3** 提供 prompt section hook,让插件插入提示词片段(master prompt、mode prompt、planner 规则、环境提示等)
- [ ] **T5** 提供 `register_uninstall_hook`,替换 `PluginLoader.unload_plugin` monkey-patch
- [ ] **T7** 修复 plugin validator 的相对 import / `submodule_search_locations`
- [ ] **T9** 提供 ReAct lifecycle hook,让插件在 reason / act / summarize 三个阶段观察,无需 override 这三个方法
- [ ] **T11** 对齐现有 skill system 是否已满足 typed `register_skill_provider`
- [ ] **T13** 修复 `execute_shell_command` heredoc handling(host shell-tool bug)

**配对说明**:T2 依赖 T1(T1 单独落地对 plugin 没有可见价值)。两者必须一起落地,才能让 DataPaw 删掉现有的 `print()` override 和 `ConsoleChannel.stream_one` frame rewrite。

**Plugin 侧后续清理(DataPaw 在同 PR 周期交付):**

- [ ] **T4** DataPaw 注册 prompt sections,并删除 `_build_sys_prompt` override(依赖 T3)
- [ ] **T6** DataPaw 注册 uninstall cleanup hook,并删除 `PluginLoader.unload_plugin` monkey-patch(依赖 T5)
- [ ] **T8** DataPaw 改回标准相对 import,并删除 `sys.path.insert` / 双路径 import workaround(依赖 T7)
- [ ] **T10** DataPaw 注册 trace append callback,并删除 `_reasoning` / `_acting` / `_summarizing` overrides(依赖 T9)
- [ ] **T12** 如果 host 暴露 typed skill provider,DataPaw 改用该 API,并删除 copytree / manifest patch / mtime cache 逻辑(依赖 T11)

### 逐项细节

#### T1 — Host: Event / Msg schema 增加 `metadata` 字段并 propagate

> **不需要 plugin hook**。T1 是纯 host 侧的 schema + 默认行为变更,写入值的 hook 在 T2。

##### 现状问题

一个出站 `Msg` 会被上游 Msg→Event 转换器扩展成多个 wire `Event`:

```
Msg(id=M1, metadata={graph_id, node_id})

└─► Event { object="message", id=M1, metadata={graph_id, node_id} }
Event { object="content", msg_id=M1, metadata=∅ } ← 问题所在
Event { object="content", msg_id=M1, metadata=∅ } ← 问题所在
Event { object="content", msg_id=M1, metadata=∅ } ← 问题所在
```

今天有两处问题:

1. `agentscope_runtime.engine.schemas.agent_schemas.Event` 根本没有 `metadata` 字段,只暴露 `sequence_number / object / status / error`。
2. 即使加了字段,默认 Msg→Event 转换器只会把 metadata 挂在头部的 `message` event 上,同一个 Msg 派生出的 `content`(delta)event 不会继承。

这就是 DataPaw 今天必须包 `ConsoleChannel.stream_one`、对每个 SSE 帧 JSON decode、按 `msg_id` 缓存 `message` 帧的 `metadata`、再注入到每个 `content` 帧的根本原因。

##### 请求

这是两项 host 内部变更,都不暴露新的 plugin API:

**1. Schema** —— 给 event / message schema 加 `metadata` 字段:

```python
class Event(BaseModel):
sequence_number: int
object: str
status: str
error: str | None = None
metadata: dict[str, Any] = Field(default_factory=dict)
```

**2. 默认 propagation** —— Msg→Event 转换器必须把 `msg.metadata` 复制到**该 Msg 派生的每一个 event** 上,包括所有 `content` delta event。这是纯机械动作,所有插件需要的行为一致,因此**不需要 hook**:

```python
def msg_to_events(msg: Msg) -> Iterable[Event]:
head = Event(object="message", id=msg.id, metadata=dict(msg.metadata))
yield head
for delta in split_into_content_deltas(msg):
yield Event(
object="content",
msg_id=msg.id,
metadata=dict(msg.metadata), # ← 必须复制
...
)
```

现有 serializer 不需要改 —— `model_dump_json()` 已经输出所有字段。

##### 收益

前端可以从每一个 chat event(`message` 和 `content` 都行)直接读路由信息:

```ts
const graphId = event.metadata?.graph_id
const nodeId = event.metadata?.node_id
```

配合 T2,DataPaw 同时删除 `print()` override 和 `ConsoleChannel.stream_one` 的 frame rewrite。

#### T2 — Host: 出站 metadata enricher hook

> **依赖 T1**。T1 放置字段,T2 让插件填值。T1 + T2 必须一起落地,才能拿掉 DataPaw 当前的 workaround。

##### 现状问题

T1 只放置字段,字段值仍需在 message 离开 agent 之前写入。不同插件写不同的 key(DataPaw 写 `graph_id` / `node_id`,别的插件可能写别的),所以写入这一步是 per-plugin 逻辑,**需要 hook**。

DataPaw 今天没有 hook,是通过 override `agent.print()` 实现的:

```python
async def print(self, msg, last=True, speech=None):
if isinstance(msg, Msg):
msg = self._annotate_msg_node_id(msg) # 写 msg.metadata
return await super().print(msg, last, speech=speech)
```

每个想附加 outbound metadata 的插件都要 subclass agent 或 monkey-patch `print()` —— 互相会踩。

##### 请求

在 plugin API 上暴露 outbound metadata enricher hook:

```python
api.register_message_metadata_enricher(
name="datapaw_node_tag",
when="outbound",
agent_id="datapaw",
enrich=_datapaw_node_enrich,
)
```

Enricher 协议:

```python
def _datapaw_node_enrich(msg, ctx) -> dict:
runtime_state = ctx.agent.plan_notebook # DataPaw 的 RuntimeStateManager
metadata = {}
if runtime_state.current_graph_id:
metadata["graph_id"] = runtime_state.current_graph_id
if runtime_state.current_node_id:
metadata["node_id"] = runtime_state.current_node_id
return metadata
```

`ctx` 至少需要暴露 `agent`(或 `session_id`),让 enricher 能反查插件自己的 state —— host 并不知道 DataPaw 的图状态,没法在 `ctx` 上直接给。

host 在 `agent.print()` 内部(`super().print()` 之前 / Msg 交给 Msg→Event 转换器之前)调用所有注册的 enricher,把返回 dict merge 到 `msg.metadata`。之后由 T1 的 propagation 规则把 metadata 自动带到每一个派生 Event。

##### 收益

DataPaw 同时删除:

- `DataPawAgent.print()` override 与 `_annotate_msg_node_id`。
- `ConsoleChannel.stream_one` 的 frame rewrite(`_wrap_stream_one` / `_maybe_inject_node_metadata`)。

插件只写 `msg.metadata`,不读 / 改 wire frame。

#### T3 — Host: Prompt section registry

##### 现状问题

DataPaw 的系统提示需要在宿主基础 prompt 之后插入若干额外片段:

- DataPaw master prompt。
- mode prompt(agent mode / plan mode)。
- planner 规则。
- 分析环境提示(沙箱配置、workspace 路径等)。

今天通过重写 `_build_sys_prompt` 实现。多个插件如果都想插入 prompt section,只能靠继承顺序串成链,无法规模化。

##### 请求

暴露一个命名、有序的 prompt section registry:

```python
api.register_prompt_section(
name="datapaw.master",
after="profile",
agent_id="datapaw",
provider=lambda agent: read_master_prompt(agent.lang),
)

api.register_prompt_section(
name="datapaw.env_hint",
after="datapaw.planner",
agent_id="datapaw",
provider=lambda agent: analysis_environment_hint(agent),
)
```

host 把自己的核心段命名(如 `agents` / `soul` / `profile` / `env_context`),按 `after=` 拓扑排序。`provider` 回调接收完整 agent 或一个受限 `PromptContext`,至少需要暴露 `mode` / 语言 / sandbox 配置 / workspace 信息 —— DataPaw 这些都需要。条件包含不需要额外 API:provider 返回空串即可。

##### 收益

T4 作为 follow-up 一起落地:DataPaw 把所有 prompt 片段迁移到 `register_prompt_section`,删除 `DataPawAgent._build_sys_prompt`。

#### T4 — Plugin: 注册 prompt sections,删除 `_build_sys_prompt` override

依赖 T3。落地后 DataPaw 的 master prompt、mode prompt、planner 规则、环境提示全部改走 `register_prompt_section`,`DataPawAgent._build_sys_prompt` 整体删除。

#### T5 — Host: `register_uninstall_hook`

##### 现状问题

DataPaw 卸载时需要清理它创建的 agent profile、`agent.json`、workspace / skills 目录,否则下一个用户会看到指向已不存在插件的残留 agent。

今天通过 monkey-patch `PluginLoader.unload_plugin` 实现:

```python
PluginLoader.unload_plugin = _wrap_unload_plugin(
PluginLoader.unload_plugin,
)
```

多插件并存时互相包同一个方法不可持续 —— 调用顺序无法保证。

##### 请求

在 plugin API 上暴露 uninstall hook:

```python
api.register_uninstall_hook(
plugin_id="datapaw",
hook_name="datapaw_cleanup",
callback=uninstall_builtin_agents,
priority=50,
)
```

`PluginLoader.unload_plugin` 按优先级调用已注册 hook,再执行自身 bookkeeping:

```python
async def unload_plugin(plugin_id: str):
for hook in api.uninstall_hooks_for(plugin_id):
await maybe_await(hook.callback())
await _unload_plugin_bookkeeping(plugin_id)
```

`register_uninstall_hook` 覆盖插件生命周期,与 `register_shutdown_hook`(进程生命周期)并存、不互相替代。

##### 收益

T6 作为 follow-up 一起落地:DataPaw 删掉 `PluginLoader.unload_plugin` monkey-patch。

#### T6 — Plugin: 注册 uninstall cleanup hook,删除 monkey-patch

依赖 T5。落地后 DataPaw 删除 `PluginLoader.unload_plugin = _wrap_unload_plugin(...)`。

#### T7 — Host: 修复 plugin validator 的相对 import

##### 现状问题

插件包内部模块应该能用标准相对 import:

```python
from .constants import PLUGIN_DIR
from .agents_setup import ensure_builtin_agents
```

但安装 validator 用 `spec_from_file_location` 加载入口文件时没有传 `submodule_search_locations`,导致插件包内的相对 import 在 validation 阶段失败。DataPaw 在两处 workaround:

```python
sys.path.insert(0, str(PLUGIN_DIR))

if __package__:
from .constants import PLUGIN_DIR
else:
from constants import PLUGIN_DIR
```

多插件并存时,`constants.py` / `utils.py` 这类自然命名模块可能按 `sys.path` 顺序撞车。

##### 请求

validator 与 runtime loader 都以 package 形态加载插件:

```python
spec = importlib.util.spec_from_file_location(
module_name,
backend_entry_file,
submodule_search_locations=[str(plugin_dir)],
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
```

这不是新增 plugin API,而是 host loader bugfix。

##### 收益

T8 作为 follow-up 一起落地:DataPaw 删除 `sys.path.insert` 和双路径 import block。

#### T8 — Plugin: 改回标准相对 import

依赖 T7。落地后删除 `constants.py` 顶部的 `sys.path.insert(0, str(PLUGIN_DIR))`,以及 `plugin.py` 里 `if __package__ ... else ...` 的双路径 import block。

#### T9 — Host: ReAct lifecycle hook

##### 现状问题

DataPaw 当前重写 `_reasoning` / `_acting` / `_summarizing`,唯一目的是把 `msg` 追加到 in-progress node 的 trace 中:

```python
async def _reasoning(self, *args, **kwargs):
msg = await super()._reasoning(*args, **kwargs)
self.plan_notebook.append_to_trace(msg)
return msg
```

`_acting` / `_summarizing` 几乎一模一样。每个 override 只有一行 side effect,纯粹为了拿一个观察点就 override 三个私有方法很脆弱:host 任何签名变更都强制插件跟着改。

##### 请求

在三个 phase 退出前 emit 统一 lifecycle event,并提供注册 API:

```python
api.register_react_lifecycle_hook(
agent_id="datapaw",
phase="after_reason", # / after_act / after_summarize
callback=_datapaw_append_to_trace,
)
```

host 侧 dispatch sketch:

```python
async def _emit_lifecycle(self, phase: str, msg):
callbacks = _lifecycle_registry.get((self.agent_id, phase), [])
ctx = {
"phase": phase,
"session_id": self.session_id,
"request_id": getattr(self, "request_id", None),
}
for cb in callbacks:
try:
result = cb(self, msg, ctx)
if inspect.isawaitable(result):
await result
except Exception:
logger.exception("react lifecycle callback failed")
```

callback 协议:`callback(agent, msg, ctx) -> None | Awaitable[None]`,callback 错误必须隔离,不能影响主 ReAct 流程。

##### 收益

T10 作为 follow-up 一起落地:DataPaw 删除三个方法 override。

#### T10 — Plugin: 注册 trace append callback,删除三个 override

依赖 T9。落地后 `DataPawAgent._reasoning` / `_acting` / `_summarizing` 全部删除。

#### T11 — Host: 对齐 typed `register_skill_provider`

##### 现状问题

DataPaw 自带 skills,需要在 workspace 中物化,并写入 host skill manifest(`enabled=True`、`source="plugin:datapaw"`、按 channel 默认启用策略等)。今天插件手工做:

1. `shutil.copytree` 把 plugin skills 拷到 workspace。
2. 调 host 内部 `reconcile_workspace_manifest`。
3. 直接读写 `skill.json`,patch `enabled` / `channels` / `source` 字段。
4. 维护 `.datapaw_versions.json` 做 mtime 缓存,决定哪些 skill 需要刷新。

这让插件耦合到 host manifest 文件结构和字段名。

host `skill_system` 已经有 `reconcile_workspace_manifest` / `get_workspace_skills_dir` / `get_builtin_skills_dir` 等构件,但 plugin 一侧的 API 还没有 typed `register_skill_provider`。

##### 请求

对齐:是否把现有 skill system 以 plugin API 形式暴露,并明确 provider 契约:

```python
api.register_skill_provider(
SkillProvider(
plugin_id="datapaw",
source="plugin:datapaw",
skills_dir=PLUGIN_DIR / "skills",
enabled_by_default=True,
channels=["all"],
)
)
```

host 负责:

- 拷贝 / 挂载 skill 目录。
- reconcile manifest。
- 应用默认启用策略。
- 卸载时按 `source` 清理。

##### 收益

T12 在 host 表态后作为 follow-up 一起落地。

#### T12 — Plugin: 迁移到 typed skill provider

依赖 T11。落地后 DataPaw 删除:

- `shutil.copytree` 把 plugin skills 拷到 workspace 的逻辑。
- 直接 patch `skill.json` 的 `enabled` / `channels` / `source` 字段。
- `.datapaw_versions.json` 的 mtime 缓存。

#### T13 — Host: 修复 `execute_shell_command` heredoc handling

##### 现状问题

ReAct agent 很自然会生成 heredoc,特别是 inline Python:

```bash
python3 << 'PY'
import pandas as pd
print(pd.__version__)
PY
```

DataPaw 在数据加载、清洗、绘图场景高频触发。

当前 `execute_shell_command` 中的 `_collapse_newlines_outside_quotes` 把 heredoc body 折叠成单行:

```bash
python3 << 'PY' import pandas as pd print(pd.__version__) PY
```

`sh -c` 于是把 `import` 当成参数,heredoc body 为空,命令 100% 失败。

##### 复现案例(glm-5.1 实测)

LLM 发出的 tool call:

```json
{
"command": "cd && python3 << 'PYEOF'\nimport pandas as pd\nimport numpy as np\nimport os\n\n# Load data\ndf = pd.read_csv('media/sales_orders.csv')\n\n# --- Validation ---\nprint(\"=== 基本信息 ===\")\nprint(f\"行数: {df.shape[0]}, 列数: {df.shape[1]}\")\nprint(f\"缺失值统计:\")\nmissing = df.isnull().sum()\nprint(missing[missing > 0] if missing.sum() > 0 else \"无缺失值\")\n\nprint(\"\\n=== 数值列异常检查 ===\")\nnumeric_cols = ['quantity', 'unit_price', 'discount_rate', 'payment_amount', 'cost', 'shipping_fee']\nfor col in numeric_cols:\n negatives = (df[col] < 0).sum()\n if negatives > 0:\n print(f\"{col}: 有 {negatives} 个负值\")\n else:\n print(f\"{col}: 无负值\")\n\n# --- Parse date ---\ndf['order_date'] = pd.to_datetime(df['order_date'])\nprint(f\"\\n日期范围: {df['order_date'].min()} ~ {df['order_date'].max()}\")\n\n# --- Compute derived metrics ---\ndf['gross_profit'] = df['payment_amount'] - df['cost']\ndf['profit_margin'] = df['gross_profit'] / df['payment_amount']\ndf['avg_order_value'] = df['payment_amount'] / df['quantity']\n\n# --- Save cleaned data ---\nsession_id = \"1779948066273\"\ngraph_id = \"graph_Zazottr9\"\nnode_id = \"data_load\"\nout_dir = f\"artifacts/{session_id}/{graph_id}/{node_id}\"\nos.makedirs(out_dir, exist_ok=True)\ndf.to_csv(f\"{out_dir}/sales_orders_cleaned.csv\", index=False)\n\n# --- Summary stats ---\nprint(f\"总销售额(GMV): {df['payment_amount'].sum():.2f}\")\nprint(f\"总订单数: {df.shape[0]}\")\nprint(f\"整体毛利率: {df['gross_profit'].sum() / df['payment_amount'].sum():.4f}\")\nPYEOF"
}
```

Tool 返回:

```text
[{"type": "text", "text": "Command failed with exit code 2.\n[stderr]\npython3: can't open file '/import': [Errno 2] No such file or directory"}]
```

错误信息就是直接证据:`_collapse_newlines_outside_quotes` 把 heredoc body 压成一行之后,`python3` 把 `import`(`<< 'PYEOF'` 之后的第一个 token)当成了位置参数,试图把 `/import` 当作脚本文件打开。

这个 bug 与模型无关 —— 任何会发出多行 heredoc 的模型都会撞到。glm-5.1 只是这次复现用到的模型。

##### 请求

在 `_collapse_newlines_outside_quotes` 中加 heredoc 状态机:

1. 扫到 `<<` 或 `<<-` 时解析 delimiter。
2. 进入 heredoc 状态后保留所有换行。
3. 直到遇到单独一行 delimiter 才退出 heredoc 状态。

调用方不需要新 API。

##### 收益

heredoc 在 `execute_shell_command` 中工作,不止 DataPaw 受益。建议作为独立 host shell-tool bug 跟踪,不与 plugin API 工作捆绑。

### 落地分组建议

1. **第一组(DAG / Chat 流拆分)**:T1 + T2。让 plugin 删掉 `ConsoleChannel.stream_one` frame rewrite 和 `print()` override。
2. **第二组(替换 runner / loader monkey-patch)**:T5 + T6、T7 + T8。
3. **第三组(替换继承型 override)**:T3 + T4、T9 + T10。
4. **后续 / 待对齐**:T11 + T12(依赖 host 表态)、T13(独立 shell-tool bug)。

## Alternatives Considered

- **保留现有 workaround**。今天能跑,但扩不到 DataPaw 之外:第二个需要同样扩展点的插件只能围绕同一批私有符号(`QwenPawAgent` 名字、`PluginLoader.unload_plugin`、`ConsoleChannel.stream_one`、agent 的 `_reasoning` / `_acting` / `_summarizing`)按未指定的顺序互相包。host 新版本随时可能静默打破 monkey-patch。
- **改成暴露 `register_agent_class` registry,而不是 per-capability hook**。否决,因为这会强迫每个想定制 agent 一小块行为(只想接 trace、只想接 prompt、只想接 outbound metadata)的插件 subclass 整个 agent,把 host 私有 surface 全部继承下来。当前方案让 host agent 类继续是权威,多个插件可以组合正交的 hook。
- **T1 的另一种做法:metadata 只放 `message` event,让前端按 `msg_id` 反查最近一帧 `message`**。否决 —— 这就是 DataPaw 今天在 `ConsoleChannel.stream_one` wrapper 里做的事,等于把同样的反查表强加到每个流消费方身上;而 propagation 规则纯机械、所有插件一致,应该在 host 转换器里做一次,不应该在每个消费方各做一次。
- **T2 的另一种做法:从 channel 层直接往 wire `Event` 写 metadata,而不是在 agent 边界写到 `Msg` 上**。否决 —— 这样 enricher 反而需要感知一个 Msg 派生的每种 event 类型;正确路径是只写一次源对象,再由 T1 propagation 带下去。
- **T13 的另一种做法:在插件侧(DataPaw 的 shell tool)修 heredoc**。否决 —— `execute_shell_command` 是 host shell tool,所有 ReAct agent 都在用,不止 DataPaw。修在 host 让所有下游受益。

## Additional Context

- DataPaw 当前实现位置(在 DataPaw 插件仓库内):`plugins/bundle/datapaw/core/agents/base.py`(smart factory、`_build_sys_prompt`、`_reasoning` / `_acting` / `_summarizing`、`print()` override),`plugins/bundle/datapaw/hooks.py`(`PluginLoader.unload_plugin` monkey-patch、`_wrap_stream_one` / `_maybe_inject_node_metadata` SSE 帧 rewrite),`plugins/bundle/datapaw/constants.py`(`sys.path.insert`),`plugins/bundle/datapaw/plugin.py`(双路径 import)。
- 文中引用的 host 侧文件:`src/qwenpaw/app/channels/console/channel.py`(`ConsoleChannel.stream_one`);上游 Msg / Event schema 在 `agentscope_runtime.engine.schemas.agent_schemas`。
- T13 的 heredoc bug 在 DataPaw 日常使用(数据加载 / 清洗 / 绘图)中频繁触发;上面的 reproducer 就是从正常 session 抓出来的。

## Acceptance

每项 host TODO 落地后,DataPaw 会在同一 PR 周期内交付依赖它的插件侧清理(T4 / T6 / T8 / T10 / T12),并在 PR 描述中链回本 issue 对应 checkbox。本 issue 在所有 checkbox 勾完后视作完成。

## Willing to Contribute

- [x] I am willing to open a PR for this feature (after discussion).

DataPaw 维护方会承担插件侧清理 PR(T4 / T6 / T8 / T10 / T12),与对应 host 变更在同一 PR 周期内交付。我们也乐意 review host 侧 PR(T1 / T2 / T3 / T5 / T7 / T9 / T11 / T13),并在有助于评估的地方提供 DataPaw 的具体使用数据。

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.