跳到內容

Pedelec SDK

用安全且具型別的橋接方式,讓本機 AI agent 與 Web App 協作。

Pedelec 是一套 browser SDK 與本機橋接架構,讓 Web App 可以和 Codex、Antigravity、OpenCode、Cursor、Claude Code,或使用 Ollama 的 agent 協作。

Web App 可以透過 Pedelec:

  • 在使用者電腦上建立 agent session;
  • 傳送使用者指令並接收串流輸出的文字;
  • 將範圍明確的前端能力以 tool 的形式提供給 agent;
  • 恢復或結束 session;
  • 在 UI 顯示連線、網站核准、provider 與 session lifecycle 狀態。

一般聊天 API 通常只接收文字再回傳文字,但 agent 整合經常需要更多能力:

  • agent 可能需要讀取目前頁面、編輯器、canvas、選取內容或應用程式 state;
  • agent turn 暫停時,Web App 可能需要開啟對話框等待使用者確認;
  • 使用者可能想沿用已經安裝並登入的本機 provider CLI;
  • 瀏覽器頁面不應直接取得任意啟動本機 process 的權限。

Pedelec 將責任拆開。Web App 負責 UI 與 tool handler;Pedelec Extension 與 Desktop Runtime 負責本機傳輸、session lifecycle 以及 provider process。

Web App
↓ @kaoruisaac/pedelec
Pedelec Chrome Extension
↓ Chrome Native Messaging
Pedelec native host
↓ 本機 Core IPC
Pedelec Desktop Runtime
↓ provider process
Codex / Antigravity / OpenCode / Cursor / Claude Code / Ollama

上圖是 control/event path。Asset file body 使用另一條內部路徑:Web App fetch(PUT/GET) → Desktop-managed 127.0.0.1 loopback asset transfer server → session .pedelec-runtime/assets/。只使用 asset API,不要依賴內部 URL、port、token 或 ticket lifecycle。

SDK 不會要求 Web App 自行啟動 localhost server,也不會直接啟動 provider executable。它只和已安裝的 Extension 溝通,再由 Extension 將通過使用者核准的要求送往 Desktop Runtime。


Pedelec SDK 必須在瀏覽器頁面環境中執行,並且需要:

  1. 使用者已安裝 Pedelec Chrome Extension。
  2. 使用者安裝後已至少正常開啟一次 Pedelec Desktop App,完成 binary setup、Native Messaging host registration 與 launch config 建立。
  3. 完成初始化後,native host 可在 Core request 時嘗試 background 啟動 Desktop;此操作可能失敗,應視為 Desktop/installation unavailable 處理。
  4. 目標 provider 在使用者本機可用。CLI 型 provider 使用 codexagyopencodecursor-agentclaude;Ollama provider 使用 Pedelec 隨附的 pedelec-agent

SDK 不適合直接在 Node.js、SSR server 或 background worker 裡使用;它需要 Chrome 頁面環境中的 extension runtime messaging。


安裝已發布的套件:

Terminal window
npm install @kaoruisaac/pedelec

在 Web App 端引用:

import { Pedelec, defineTool } from "@kaoruisaac/pedelec";

import { Pedelec, defineTool } from "@kaoruisaac/pedelec";
const pedelec = new Pedelec();
const session = await pedelec.createSession({
provider: "codex",
effortLevel: "high",
skills: {
guidance: "需要瀏覽器頁面資訊時使用 get_current_page。",
tools: [
defineTool({
name: "get_current_page",
description: "讀取目前瀏覽器頁面的 title 與 URL。",
argsSchema: {
type: "object",
properties: {},
required: [],
},
handler: () => ({
url: location.href,
title: document.title,
}),
}),
],
},
});
session.onChat((text) => {
// 一個完成的 logical assistant message
console.log(text);
});
session.onChatDelta((delta) => {
// live UI 可選用的 best-effort 文字增量
console.log(delta);
});
session.onStatus((status) => {
// idle | running | waiting_tool_result | ended | error
console.log("status", status);
});
session.onError((error) => {
console.error(error.code, error.message, error.details);
});
await session.sendText("請幫我分析目前頁面的狀態");

sendText() 會在本輪 agent 回應完成後 resolve;如果 session 已經在處理上一個 prompt,新的 sendText() 會被拒絕,避免同一個 session 同時跑多個請求。


首次在某個 origin 呼叫 createSession()resumeSession() 時,extension 會要求使用者在 popup 中核准該 origin。核准後同一個 origin 之後可直接建立 session。

可以先查詢目前 origin 的 approval 狀態,用來決定是否顯示「Connect Pedelec」類 UI:

const status = await pedelec.getApprovalStatus();
console.log(status.installed, status.approved, status.origin);
const session = await pedelec.createSession({
provider: "opencode",
effortLevel: "high",
});

目前 SDK 支援的 provider code:

Provider Code
Codex codex
Antigravity antigravity
OpenCode opencode
Cursor cursor
Claude Code claude
Ollama ollama

Ollama session 使用 Desktop 設定的 endpoint 與內附 pedelec-agent。選中的 effort profile 必須有 model;空 profile 會回傳 MODEL_REQUIRED,不會 fallback 到 default

Desktop Settings 也可替 Ollama session 設定 optional Tavily API key。設定後,內附 agent 可自行判斷是否使用 Tavily web search;目前固定使用 basic search depth,每次最多回傳五筆結果。未設定 key 時,不會向 Ollama model 提供 web-search tool。

如果 Desktop App 已設定 default provider,可以不傳 provider

const session = await pedelec.createSession({
skills: {
guidance: "使用者要求更新 counter 時使用 update_counter。",
tools: [
defineTool({
name: "update_counter",
description: "依照 delta 更新畫面上的 counter。",
argsSchema: {
type: "object",
required: ["delta"],
properties: {
delta: {
type: "number",
description: "Counter delta.",
},
},
},
}),
],
},
});

這會只讀取 Desktop App 的 defaultProvider。如果沒有設定 default provider,SDK 會拋出 DEFAULT_PROVIDER_NOT_SET;具體 effort argv 由 Core 解析 Desktop 設定。

const session = await pedelec.createSession({
provider: "codex",
});

只傳 provider 時,SDK 使用 effortLevel: "default"。非 Ollama provider 可使用空 profile;Ollama 的選中 profile 沒有 model 時會回傳 MODEL_REQUIRED

SDK 建立的 session 預設是 page-scoped。autoEndOnDisconnect 預設為 true,因此頁面重新整理、關閉 tab,或該 session 的最後一個 SDK connection 中斷時,Pedelec 會自動結束對應的 Desktop thread。

Demo 與 page-scoped app 建議使用預設值。若需要在頁面切換後 resumeSession,或跨頁共用同一個 session,請明確設定 autoEndOnDisconnect: false

const session = await pedelec.createSession({
provider: "codex",
autoEndOnDisconnect: false,
});

const providers = await pedelec.listProviders();
for (const provider of providers) {
console.log(provider.code, provider.available, provider.isDefault, provider.error);
}

回傳格式:

type ProviderInfo = {
name: string;
code: "codex" | "antigravity" | "opencode" | "cursor" | "claude" | "ollama";
available: boolean;
isDefault: boolean;
error: string | null;
};

isDefault 表示 Desktop settings 選擇的 provider,與 available 無關;即使 default provider unavailable 也仍是 trueavailable: false 通常代表該 provider CLI 沒有安裝,或不在 PATH 裡。 對 Ollama 而言,available: true 只代表 Pedelec 找得到 pedelec-agent;不代表 endpoint 可連線、credential 有效,或指定 model 已下載。


const settings = await pedelec.getSettings();
console.log(settings.defaultProvider);

回傳格式:

type PedelecSettings = {
defaultProvider: "codex" | "antigravity" | "opencode" | "cursor" | "claude" | "ollama" | null;
};

使用 onChat() 接收完成的 logical assistant message:

session.onChat((text) => {
saveCompletedAssistantMessage(text);
});

如果要做即時串流 UI,另外使用 onChatDelta() 累積文字增量:

const chunks: string[] = [];
session.onChatDelta((delta) => {
chunks.push(delta);
render(chunks.join(""));
});

Delta delivery 是 best-effort,而且依 Provider 能力而定。Delta boundary 沒有語意,Pedelec 也不會根據後續 completed message 自行合成缺少的 suffix delta。


session.onStatus((status) => {
switch (status) {
case "idle":
break;
case "running":
break;
case "waiting_tool_result":
break;
case "ended":
break;
case "error":
break;
}
});

常見狀態:

狀態 意義
idle session 可接收下一個 prompt
running agent 正在處理使用者輸入
waiting_tool_result agent 發出 tool call,正在等待前端回傳結果
ended session 已結束
error session 發生錯誤

Public SDK 透過 session.onError((error, ctx) => ...) 回報錯誤。errorPedelecErrorctx.source 表示 callback 是由 Core event 還是 SDK-side handling 產生:

  • "core":錯誤事件來自 Core/provider execution path;
  • "sdk":SDK 在處理 local request、disconnect 或 protocol condition 時產生錯誤。
type ErrorEventContext = PedelecEventContext & {
type: "error" | "sdk_error";
source: "core" | "sdk";
};
session.onError((error, ctx) => {
console.error(ctx.source, error.code, error.message, error.details);
});

目前公開 callback context 不會另外提供 provider responsibility 欄位。不要判斷 ctx.source === "provider",也不要預期存在 ctx.provider

getSettings()listProviders() 需要 origin approval 且可能開啟 popup。Settings 絕不公開 provider credentials,而 provider entry 僅含上述欄位;SDK 會忽略 Extension response 中新增的未知欄位,以保持 forward compatibility。請以 getApprovalStatus().appConnected 做非敏感 Desktop connectivity probe。


Pedelec 的 tool calling 流程是:

  1. Web App 在 createSession 提供 skills: { guidance, tools }
  2. Desktop Runtime 驗證 manifest、在記憶體保存 ToolRegistry,並在首次 provider prompt 直接注入 guidance 與 tool index。
  3. Agent 需要前端資料或操作時,用 pedelec-cli --thread-id <pedelec_thread_id> tool-spec <tool> 取得完整 schema,再執行 pedelec-cli --thread-id <pedelec_thread_id> tool-call ...
  4. Desktop Runtime 收到 tool call 後,透過 native host 與 extension 送回 SDK。
  5. SDK 觸發 session.onTool()
  6. Web App 執行對應工具並 return result。
  7. SDK 自動把 result submit 回 Desktop Runtime,再交給 agent 繼續推理。

每個 defineTool 使用 argsSchema 描述 tool arguments,這份描述會送給 provider / agent。root schema 必須是 object。argsSchema 是 Pedelec Tool Args Schema subset,不是完整 JSON Schema;它支援常見的 stringnumberintegerbooleanarrayobjectoneOf node,以及 descriptiondefaultexamplesenum、數值範圍、array 長度限制與 required 等欄位。default 只是給 agent 的提示,SDK 不會自動補值。舊的 shorthand input schema 不再支援。第一版也不支援 $defs$refadditionalPropertiesexclusiveMinimumexclusiveMaximummultipleOfformat;需要重用 schema 時請用 TypeScript const。

範例:

session.onTool(async (tool, args) => {
if (tool === "get_current_page") {
return {
url: location.href,
title: document.title,
selectedText: window.getSelection()?.toString() ?? "",
};
}
if (tool === "update_counter") {
const { delta } = args as { delta: number };
counter.value += delta;
return {
counter: counter.value,
delta,
};
}
return {
error: {
code: "TOOL_NOT_FOUND",
message: `Unknown tool: ${tool}`,
},
};
});

onTool() 的回傳值必須可以 JSON serialize。SDK 會自動把回傳值送回 runtime,不需要手動呼叫 submit_tool_result


如果你有保存 sessionId,可以重新接回既有 session:

const session = await pedelec.resumeSession("thread_abc123");
session.onChat((text) => {
console.log(text);
});
await session.sendText("繼續剛剛的工作");

resumeSession() 是以新的 SDK handle 重新 attach 到既有 Core thread;如果該 thread 已是 ended,它仍會維持 ended,不會自動恢復。若要在明確 end() 後繼續使用原本的 handle,請使用 session.resume()

await session.end();
await session.resume();
await session.sendText("從原本的 handle 繼續工作");

同一個 handle 的 resume() 會保留 handlers、usage、provider/effort metadata、prepared 狀態與 provider session identity;成功後才由 Core snapshot 確認為 idle,provider work 仍會在下一個 operation 時 lazy 啟動。workspace 缺失或不可開啟會回傳 WORKSPACE_OPEN_FAILED;transport-detached 的舊 handle 不能 resume,請改用 resumeSession(sessionId)


await session.end();

結束後,該 session 不能再呼叫 sendText(),直到同一個 handle 成功 resume();若不再使用原本的 handle,或它已 transport-detached,請重新 resumeSession(sessionId)createSession()

結束 session 不會立即刪除其 workspace。未指定 workspace.path 的 session 使用 Desktop 管理的暫存資料:Desktop App 正常退出時會清除 managed workspace,異常結束後的殘留會在下次啟動時清理。指定 absolute workspace.path 則是 application 管理的 workspace,Pedelec 在 session 結束、App 退出或 stale cleanup 時都不會刪除它,多個 active session 可以共用。關閉主視窗只會隱藏 App,不會觸發 managed cleanup。Ended session 不能透過 asset API 存取保留的 workspace,explicit shared workspace 的 filesystem conflict 也不由 Pedelec 協調。

若啟用 autoEndOnDisconnect,disconnect cleanup 的目標與呼叫 session.end() 相同:thread 會被結束,且不再被視為 active。


建議所有 SDK 操作都包在 try/catch,並同時註冊 onError()

session.onError((error) => {
console.error("session error", error);
});
try {
await session.sendText("請幫我修改這段內容");
} catch (error) {
console.error("send failed", error);
}

常見錯誤:

code 可能原因
EXTENSION_UNAVAILABLE SDK 不在瀏覽器頁面中執行,或 extension 無法連線
EXTENSION_DISCONNECTED extension 連線中斷
SDK_BRIDGE_TIMEOUT extension 沒有在 timeout 內回應
APPROVAL_REJECTED 使用者拒絕目前 origin 使用 Pedelec
APPROVAL_TIMEOUT 使用者未在期限內完成 origin approval
OPEN_POPUP_FAILED extension 無法自動開啟 approval popup
NATIVE_HOST_UNAVAILABLE Chrome Native Messaging host 無法連線
DEFAULT_PROVIDER_NOT_SET Desktop App 尚未設定 default provider
DEFAULT_PROVIDER_UNAVAILABLE default provider 不可用
SESSION_BUSY 同一個 session 已有 prompt 正在執行
SESSION_ENDED session 已結束
TOOL_HANDLER_NOT_FOUND agent 呼叫 tool,但 Web App 沒有註冊 handler