feat(github-copilot-sdk): release v0.10.0 with native prompt restoration and live todo widget
- Restore native Copilot CLI prompts for authentic Plan Mode behavior - Add SQLite-backed session management for state persistence via system prompt - Implement Adaptive Autonomy (Agent chooses planning vs direct execution) - Fix OpenWebUI custom tool context injection for v0.8.x compatibility - Add compact Live TODO widget synchronized with session.db - Upgrade SDK to github-copilot-sdk==0.1.30 - Remove legacy mode switch RPC calls (moved to prompt-driven orchestration) - Fix intent status localization and widget whitespace optimization - Sync bilingual READMEs and all documentation mirrors to v0.10.0
This commit is contained in:
46
.agent/learnings/README.md
Normal file
46
.agent/learnings/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# `.agent/learnings/` — Engineering Learnings & Reusable Patterns
|
||||
|
||||
This directory stores **hard-won engineering insights** discovered during development.
|
||||
Each file is a standalone Markdown note covering a specific topic, pattern, or gotcha.
|
||||
|
||||
The goal is to avoid re-investigating the same issue twice.
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
- **File naming**: `{topic}.md`, e.g., `openwebui-tool-injection.md`
|
||||
- **Scope**: One clear topic per file. Keep files focused and concise.
|
||||
- **Format**: Use the template below.
|
||||
|
||||
---
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
# [Topic Title]
|
||||
|
||||
> Discovered: YYYY-MM-DD
|
||||
|
||||
## Context
|
||||
Where / when does this apply?
|
||||
|
||||
## Finding
|
||||
What exactly did we learn?
|
||||
|
||||
## Solution / Pattern
|
||||
The code or approach that works.
|
||||
|
||||
## Gotchas
|
||||
Edge cases or caveats to watch out for.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Index
|
||||
|
||||
| File | Topic |
|
||||
|------|-------|
|
||||
| [openwebui-tool-injection.md](./openwebui-tool-injection.md) | How OpenWebUI injects parameters into Tool functions, and what the Pipe must provide |
|
||||
| [openwebui-mock-request.md](./openwebui-mock-request.md) | How to build a valid Mock Request for calling OpenWebUI-internal APIs from a Pipe |
|
||||
| [copilot-plan-mode-prompt-parity.md](./copilot-plan-mode-prompt-parity.md) | Why Plan Mode prompt logic must be shared between fresh-session and resume-session injection |
|
||||
40
.agent/learnings/copilot-plan-mode-prompt-parity.md
Normal file
40
.agent/learnings/copilot-plan-mode-prompt-parity.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copilot Plan Mode Prompt Parity
|
||||
|
||||
> Discovered: 2026-03-06
|
||||
|
||||
## Context
|
||||
|
||||
The GitHub Copilot SDK pipe builds system prompts in two paths:
|
||||
|
||||
- fresh session creation via `_build_session_config(...)`
|
||||
- resumed session injection via the `system_parts` rebuild branch
|
||||
|
||||
Plan Mode guidance was duplicated across those branches.
|
||||
|
||||
## Finding
|
||||
|
||||
If Plan Mode instructions are edited in only one branch, resumed sessions silently lose planning behavior or capability hints that fresh sessions still have.
|
||||
|
||||
This is especially easy to miss because both branches still work, but resumed chats receive a weaker or stale prompt.
|
||||
|
||||
Session mode switching alone is also not enough. Even when `session.rpc.mode.set(Mode.PLAN)` succeeds, the SDK may still skip creating the expected `plan.md` if the runtime system prompt does not explicitly include the original Plan Mode persistence contract.
|
||||
|
||||
## Solution / Pattern
|
||||
|
||||
Extract the Plan Mode prompt into one shared helper and call it from both branches:
|
||||
|
||||
```python
|
||||
def _build_plan_mode_context(plan_path: str) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
Then inject it in both places with the chat-specific `plan.md` path.
|
||||
|
||||
For extra safety, when the pipe later reads `session.rpc.plan.read()`, mirror the returned content into the chat-specific `COPILOTSDK_CONFIG_DIR/session-state/<chat_id>/plan.md` path. This keeps the UI-visible file in sync even if the SDK persists plan state internally but does not materialize the file where the chat integration expects it.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Keep the helper dynamic: the `plan.md` path must still be resolved per chat/session.
|
||||
- Do not only update debug prompt artifacts; the effective runtime prompt lives in `plugins/pipes/github-copilot-sdk/github_copilot_sdk.py`.
|
||||
- Resume-session parity matters for capability guidance just as much as for session context.
|
||||
- If users report that Plan Mode is active but `plan.md` is missing, check both halves: prompt parity and the final `rpc.plan.read()` -> `plan.md` sync path.
|
||||
131
.agent/learnings/openwebui-mock-request.md
Normal file
131
.agent/learnings/openwebui-mock-request.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Building a Valid Mock Request for OpenWebUI Pipes
|
||||
|
||||
> Discovered: 2026-03-05
|
||||
|
||||
## Context
|
||||
|
||||
OpenWebUI Pipes run as a Pipe plugin, not as a real HTTP request handler. When the Pipe
|
||||
needs to call OpenWebUI-internal APIs (like `generate_chat_completion`, `get_tools`, etc.)
|
||||
or load Tools that do the same, it must provide a **fake-but-complete Request object**.
|
||||
|
||||
## Finding
|
||||
|
||||
OpenWebUI's internal functions expect `request` to satisfy several contracts:
|
||||
|
||||
```
|
||||
request.app.state.MODELS → dict { model_id: ModelModel } — MUST be populated!
|
||||
request.app.state.config → config object with all env variables
|
||||
request.app.state.TOOLS → dict (can start empty)
|
||||
request.app.state.FUNCTIONS → dict (can start empty)
|
||||
request.app.state.redis → None is fine
|
||||
request.app.state.TOOL_SERVERS → [] is fine
|
||||
request.app.url_path_for(name, **path_params) → str
|
||||
request.headers → dict with Authorization, host, user-agent
|
||||
request.state.user → user dict
|
||||
request.state.token.credentials → str (the Bearer token, without "Bearer " prefix)
|
||||
await request.json() → dict (the raw request body)
|
||||
await request.body() → bytes (the raw request body as JSON bytes)
|
||||
```
|
||||
|
||||
## Solution / Pattern
|
||||
|
||||
```python
|
||||
from types import SimpleNamespace
|
||||
import json as _json_mod
|
||||
|
||||
def _build_openwebui_request(user: dict, token: str, body: dict = None):
|
||||
from open_webui.config import PERSISTENT_CONFIG_REGISTRY
|
||||
from open_webui.models.models import Models as _Models
|
||||
|
||||
# 1. Build config from registry
|
||||
config = SimpleNamespace()
|
||||
for item in PERSISTENT_CONFIG_REGISTRY:
|
||||
val = item.value
|
||||
if hasattr(val, "value"):
|
||||
val = val.value
|
||||
setattr(config, item.env_name, val)
|
||||
|
||||
# 2. Populate MODELS from DB — critical for model validation
|
||||
system_models = {}
|
||||
try:
|
||||
for m in _Models.get_all_models():
|
||||
system_models[m.id] = m
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Build app_state
|
||||
app_state = SimpleNamespace(
|
||||
config=config,
|
||||
TOOLS={},
|
||||
TOOL_CONTENTS={},
|
||||
FUNCTIONS={},
|
||||
FUNCTION_CONTENTS={},
|
||||
MODELS=system_models, # <-- KEY: must not be empty!
|
||||
redis=None,
|
||||
TOOL_SERVERS=[],
|
||||
)
|
||||
|
||||
# 4. url_path_for helper
|
||||
def url_path_for(name: str, **params):
|
||||
if name == "get_file_content_by_id":
|
||||
return f"/api/v1/files/{params.get('id')}/content"
|
||||
return f"/mock/{name}"
|
||||
|
||||
app = SimpleNamespace(state=app_state, url_path_for=url_path_for)
|
||||
|
||||
# 5. Async body helpers
|
||||
async def _json():
|
||||
return body or {}
|
||||
|
||||
async def _body_fn():
|
||||
return _json_mod.dumps(body or {}).encode("utf-8")
|
||||
|
||||
# 6. Headers
|
||||
headers = {
|
||||
"user-agent": "Mozilla/5.0",
|
||||
"host": "localhost:8080",
|
||||
"accept": "*/*",
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = token if token.startswith("Bearer ") else f"Bearer {token}"
|
||||
|
||||
return SimpleNamespace(
|
||||
app=app,
|
||||
headers=headers,
|
||||
method="POST",
|
||||
cookies={},
|
||||
base_url="http://localhost:8080",
|
||||
url=SimpleNamespace(path="/api/chat/completions", base_url="http://localhost:8080"),
|
||||
state=SimpleNamespace(
|
||||
token=SimpleNamespace(credentials=token or ""),
|
||||
user=user or {},
|
||||
),
|
||||
json=_json,
|
||||
body=_body_fn,
|
||||
)
|
||||
```
|
||||
|
||||
## Token Extraction
|
||||
|
||||
Tokens can be found in multiple places. Check in order:
|
||||
|
||||
```python
|
||||
# 1. Direct in body (some SDK requests embed it)
|
||||
token = body.get("token")
|
||||
|
||||
# 2. In metadata
|
||||
token = token or (metadata or {}).get("token")
|
||||
|
||||
# 3. In the original __request__ Authorization header
|
||||
if not token and __request__ is not None:
|
||||
auth = getattr(__request__, "headers", {}).get("Authorization", "")
|
||||
if auth.startswith("Bearer "):
|
||||
token = auth.split(" ", 1)[1]
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`app.state.MODELS` empty = "Model not found"** for *any* model ID, even correct ones.
|
||||
- `TOOL_SERVER_CONNECTIONS` must be synced from DB, not from in-memory cache (stale in multi-worker).
|
||||
- `request.state.token.credentials` should be the **raw token** (no "Bearer " prefix).
|
||||
- Tools may call `await request.json()` — must be an async method, not a regular attribute.
|
||||
83
.agent/learnings/openwebui-tool-injection.md
Normal file
83
.agent/learnings/openwebui-tool-injection.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# OpenWebUI Tool Parameter Injection
|
||||
|
||||
> Discovered: 2026-03-05
|
||||
|
||||
## Context
|
||||
|
||||
When OpenWebUI loads a Python Tool and calls one of its functions (e.g. `generate_mind_map`),
|
||||
it automatically matches parameters from an `extra_params` dict against the function's
|
||||
signature **by name**. This is done in:
|
||||
|
||||
```
|
||||
open_webui/utils/tools.py → get_async_tool_function_and_apply_extra_params()
|
||||
```
|
||||
|
||||
The lookup is: `extra_params = {k: v for k, v in extra_params.items() if k in sig.parameters}`
|
||||
|
||||
## Finding
|
||||
|
||||
A Tool function declares its dependencies via its parameter names. Common injected names:
|
||||
|
||||
| Parameter Name | What it contains |
|
||||
|-----------------------|---------------------------------------------------|
|
||||
| `__user__` | User context dict (id, email, role, name) |
|
||||
| `__event_emitter__` | Async callable to emit status/notification events |
|
||||
| `__event_call__` | Async callable for JS `__event_call__` roundtrips |
|
||||
| `__request__` | Request-like object (must have `.app.state.MODELS`) |
|
||||
| `__metadata__` | Dict: `{model, base_model_id, chat_id, ...}` |
|
||||
| `__messages__` | Full conversation history list |
|
||||
| `__chat_id__` | Current chat UUID |
|
||||
| `__message_id__` | Current message UUID |
|
||||
| `__session_id__` | Current session UUID |
|
||||
| `__files__` | Files attached to the current message |
|
||||
| `__task__` | Task type string (e.g. `title_generation`) |
|
||||
| `body` | Raw request body dict (non-dunder variant) |
|
||||
| `request` | Request object (non-dunder variant) |
|
||||
|
||||
## Key Rule
|
||||
|
||||
**`extra_params` must contain ALL keys a Tool's function signature declares.**
|
||||
If a key is missing from `extra_params`, the parameter silently receives its default
|
||||
value (e.g. `{}` for `__metadata__`). This means the Tool appears to work but
|
||||
gets empty/wrong context.
|
||||
|
||||
## Solution / Pattern
|
||||
|
||||
When a Pipe calls an OpenWebUI Tool, it must populate `extra_params` with **all** the above:
|
||||
|
||||
```python
|
||||
extra_params = {
|
||||
"__request__": request, # Must have app.state.MODELS populated!
|
||||
"request": request, # Non-dunder alias
|
||||
"__user__": user_data,
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
"__messages__": messages,
|
||||
"__metadata__": __metadata__ or {},
|
||||
"__chat_id__": chat_id,
|
||||
"__message_id__": message_id,
|
||||
"__session_id__": session_id,
|
||||
"__files__": files,
|
||||
"__task__": task,
|
||||
"__task_body__": task_body,
|
||||
"body": body, # Non-dunder alias
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Model Resolution
|
||||
|
||||
Tools that call `generate_chat_completion` internally need a **valid model ID**.
|
||||
When the conversation is running under a Pipe/Manifold model (e.g. `github_copilot.gpt-4o`),
|
||||
the Tool's `valves.MODEL_ID` must be a *real* model known to the system.
|
||||
|
||||
`generate_chat_completion` validates model IDs against `request.app.state.MODELS`.
|
||||
➡️ That dict **must be populated** from the database (see `openwebui-mock-request.md`).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Tools call `generate_chat_completion` with a `request` arg that must be the full Mock Request.
|
||||
- If `app.state.MODELS` is empty, even a correctly-spelled model ID will cause "Model not found".
|
||||
- `__metadata__['model']` can be a **dict** (from DB) **or a string** (manifold ID). Tools must
|
||||
handle both types.
|
||||
- For manifold models not in the DB, strip the prefix: `github_copilot.gpt-4o` → `gpt-4o`.
|
||||
@@ -138,6 +138,18 @@ Before completing an antigravity operation, confirm:
|
||||
- [ ] Database changes are idempotent (safe to re-run)
|
||||
- [ ] Timeout guards are in place for all async calls to external systems
|
||||
- [ ] The user can observe progress through status/notification events
|
||||
- [ ] Non-obvious findings / gotchas are saved to `.agent/learnings/{topic}.md`
|
||||
|
||||
---
|
||||
|
||||
## Mandatory: Knowledge Capture
|
||||
|
||||
Any non-obvious pattern, internal API contract, or workaround discovered during an
|
||||
antigravity session **MUST** be saved to `.agent/learnings/{topic}.md` before the
|
||||
session ends. This ensures hard-won insights are not lost between sessions.
|
||||
|
||||
**Format**: See `.agent/learnings/README.md`
|
||||
**Existing entries**: Browse `.agent/learnings/` for prior knowledge to reuse.
|
||||
|
||||
---
|
||||
|
||||
@@ -145,3 +157,4 @@ Before completing an antigravity operation, confirm:
|
||||
|
||||
- Full engineering spec: `.github/copilot-instructions.md` → Section: **Antigravity Development Mode**
|
||||
- Design document: `docs/development/copilot-engineering-plan.md` → Section 5
|
||||
- Knowledge base: `.agent/learnings/` — reusable engineering patterns and gotchas
|
||||
|
||||
@@ -140,6 +140,7 @@ Before committing:
|
||||
- [ ] `docs/` index and detail pages are updated?
|
||||
- [ ] Root `README.md` is updated?
|
||||
- [ ] All version numbers match exactly?
|
||||
- [ ] Any non-obvious findings saved to `.agent/learnings/{topic}.md`?
|
||||
|
||||
## 5. Git Operations (Agent Rules)
|
||||
|
||||
@@ -147,3 +148,12 @@ Before committing:
|
||||
2. **No Auto-Commit**: Never `git commit`, `git push`, or `create_pull_request` automatically after file updates unless the user explicitly says "commit this" or "release now".
|
||||
3. **Draft Mode**: If available, use PRs as drafts first.
|
||||
4. **Reference**: Strictly follow the rules defined in `.github/copilot-instructions.md` → **Git Operations (Agent Rules)** section.
|
||||
|
||||
## 6. Knowledge Capture (Mandatory)
|
||||
|
||||
Whenever you discover a non-obvious behaviour, internal API contract, or workaround
|
||||
during plugin development, **document it in `.agent/learnings/{topic}.md`** before
|
||||
ending the session.
|
||||
|
||||
- Browse `.agent/learnings/` **first** at the start of a session to reuse existing knowledge.
|
||||
- Format: see `.agent/learnings/README.md`.
|
||||
|
||||
Reference in New Issue
Block a user