概念 · Hosted Execution
執行與事件
Conversation Turn 是持久的對話工作;Hosted Run 則是 Process-local 的協調外殼,讓 Request、Subscriber、Approval 與 Cancellation 可以在工作執行期間定址同一份工作。
客製化深度
深度 4 — Conversation Engine 之上的生命週期協調
託管深度
長駐 Node.js Process;可選用 Browser-safe Remote Consumer
文件狀態
權威參考
前置假設
- 同一個 ConversationRunService Instance 在 Active Run 與 Replay Retention Window 期間持續存活。
- Host 可以建立至少含 sessionId 與授權 Scope 的可信任 Address。
- Remote Payload 在跨越 Transport Boundary 前,會透過明確的 Public Schema 投影。
- Process-local Run Identity、每個 Address 一個 Active Run、有序 Sequence、Terminal Settlement、Cancellation、Approval Resolution 與有界 Replay。
- Transport-neutral Run Consumer 的 Cursor 正確性、Duplicate Suppression、Gap Detection、Terminal Detection 與有界 Reconnect 計算。
- 選用 HTTP/SSE Entrypoint 時的 Node SSE Framing 與 Browser Fetch/SSE Parsing。
- Run Service 壽命、已驗證的 Address Scope、Start/Subscribe/Cancel 授權、Transport Route、CORS、Limit、Audit 與 Public Error。
- Public Activity 與 Result Projection、Client Cursor 持久化、實際 Retry Timer、UI State,以及產品專屬 Terminal Presentation。
- 部署拓樸需要時的 Multi-process Routing、Restart Recovery、Draining、Durable Delivery 與 Broker Infrastructure。
Turn 與 Run 的差異
Conversation Engine 可以直接在同一個 Process 中執行 Turn:
await engine.turns.submit({ sessionId, prompt, host })如果 Caller 會保持連線直到完成,這已經足夠。當 Turn 必須脫離單一 Request 被獨立定址時,再加入 ConversationRunService。例如 HTTP Request 要立刻回傳、SSE Subscriber 稍後才連線、User 要從另一個 Request 取消,或 Approval 必須等待另一個 UI Action。
import { createConversationEngine } from '@roackb2/heddle'
import { ConversationRunService } from '@roackb2/heddle/hosted'
const engine = createConversationEngine({ workspaceRoot, stateRoot, model })
const runs = new ConversationRunService({
replay: { maxEventsPerRun: 512, retentionMs: 300_000 },
})Run Service 不會取代 Engine。Engine 仍是 Session 與 Turn 語意的 Durable Owner;Run Service 只在一個 Host Process 內協調 Active Work。
先有 Address,再有 Run ID
每個 Run 有兩種 Identity:
- Address 是 Host-defined Scope,至少必須包含
sessionId; - Run ID 是 Heddle 為一次已接受的 Execution 產生的識別碼。
預設 Address 是 { scopeId, sessionId }。產品也可以定義更完整的 Address 與穩定 Key:
type ProductRunAddress = {
accountId: string
workspaceId: string
sessionId: string
}
const runs = new ConversationRunService<ProductRunAddress>({
addressKey: ({ accountId, workspaceId, sessionId }) =>
JSON.stringify([accountId, workspaceId, sessionId]),
})同一個 Address Key 一次只能有一個 Active Run。若重複 Start,Heddle 會回報 Conflict,而不是悄悄啟動兩個 Writer 同時修改同一段 Conversation。
持有 runId 絕不等於獲得授權。請先驗證 Caller、建立可信任 Address,再由 Service 在該 Address 內 Resolve、Subscribe、Cancel 或 Approve。
Start 與 Acceptance
新的 Prompt 使用 startTurn,延續語意使用 startContinue:
const run = runs.startTurn({
address: { accountId, workspaceId, sessionId },
engine,
turn: { sessionId, prompt },
projectResult(result) {
return {
outcome: result.outcome,
summary: result.summary,
}
},
projectError(error) {
return { code: 'agent_run_failed', message: safePublicMessage(error) }
},
})回傳的 Handle 包含 runId、acceptedAt、result Promise、Async events(...) Stream、cancel() 與 resolveApproval(...)。
如果 Host 必須在 Execution 開始前持久記錄或稽核已接受的 Run,請使用 Acceptance Hook。Async startAndWaitForAcceptance 路徑只會在該 Hook 成功後回報 Acceptance。
標準事件封套
每個 Published Item 都使用相同 Envelope:
{
runId: string
sequence: number
timestamp: string
kind: 'activity' | 'result' | 'cancelled' | 'error'
}Payload 依 kind 而不同:
activity帶有一個 SemanticConversationActivity;result帶有最終 Result 或 Host 投影後的 Result;cancelled帶有取消原因;error帶有 Public{ code, message }Projection。
Sequence 從 1 開始,並在單一 Run 內逐一遞增。一個 Run 可以發出多個 Activity Event,但只能有一個 Terminal Event:result、cancelled 或 error。Terminal Settlement 後 Subscriber 會關閉,之後的 Event 都是無效的。
比較 Activity 時,請使用 HeddleEventType,不要複製 assistant.stream、tool.calling、tool.completed、tool.approval_requested 或 compaction.running 等 String Literal。
Replay 與重新連線
Subscriber 重新連線時,提供最後已接受的 Sequence:
for await (const event of runs.subscribe({
address,
runId,
afterSequence: 42,
signal,
})) {
sendToClient(event)
}Service 會先 Replay Sequence 大於 42 的 Retained Item,再接上 Live Delivery。Subscriber 中斷只會移除該 Subscriber,不會取消 Run。
Replay 會同時受 Event Count 與 Retention Time 限制。如果要求的 Cursor 早於保留範圍,Heddle 會拋出 ConversationRunReplayUnavailableError。Host 應提供恢復路徑,通常是重新載入 Durable Conversation State;不可假裝遺失的 Live Interval 已經送達。
Replay Buffer 是 Process-local。它不會跨 Host Restart 保存,也不提供 Cross-instance Delivery。Durable Final Conversation State 仍存在 Engine 的 Session Repository。
Cancellation 必須勝過 Terminal Settlement
取消會 Abort Run Controller,並 Deny 所有 Pending Approval。對 startTurn 與 startContinue 而言,已要求取消後,即使 Execution 很晚才回傳結果,也不能用該結果覆寫取消;Stream 應以 cancelled 結束。
Cancellation 是明確的產品行為,不是 Subscriber Cleanup。關閉 SSE Connection 或離開頁面只應 Abort 該 Subscription。另一個經過授權的 Cancel Operation 才應呼叫 cancelRun(address, runId) 或 Handle 的 cancel()。
Approval 生命週期
當受 Gate 保護的 Tool Call 需要決策時,Run Service 可以為相同 Address 與 Run 保留 Pending Approval。另一個經授權的 Request 可以透過 Run Handle 或 resolvePendingApproval 解決它。
Service 負責等待與 Resolution Plumbing。產品負責誰是已驗證的 Approver、Policy 允許哪些 Action、Approval UI,以及某個決策是否能被記住。
Public Result 與 Error Projection
Internal Turn Result 可能包含 Tool Input、Path、Trace 與 Artifact Metadata,不應全部跨越 Remote Boundary。在 Terminal Event Publish 前,使用 projectResult 與 projectError。
Remote Client 應加入使用同步 Standard Schema Validator 的 ConversationRunProtocolCodec。Validator Output 就是 Public Allowlist。Codec 會在 Parse 或 Serialize 前驗證 Run ID、正整數 Sequence、Timestamp、Terminal Vocabulary、Host Payload 與 JSON Safety。
import { z } from 'zod'
import { ConversationRunProtocolCodec } from '@roackb2/heddle-remote'
const protocol = new ConversationRunProtocolCodec({
activity: z.object({ type: z.string().min(1) }),
result: z.object({ outcome: z.string(), summary: z.string() }),
})Codec 無法替你決定授權,也不會自動發現 Secret。嚴格的 Public Schema 與 Host-side Policy 仍是必要條件。
Remote Consumer 的不變條件
ConversationRunConsumerService 是 Transport-neutral Client State Machine。對一個已選定的 Run,它會:
- 提供 Canonical
afterSequenceCursor; - 忽略屬於其他 Run 的 Event;
- 忽略已接受過的 Duplicate Sequence;
- 發現 Sequence Gap 時拋出錯誤;
- 只在接受 Event 後推進 Cursor;
- 辨識所有三種 Terminal Kind;
- 拒絕 Terminal 後收到的 Event;
- 計算有界 Reconnect Attempt,並在有進度後重設 Retry Budget。
產品仍負責 Subscription Handle、實際 Timer、Online/Offline Policy、Cursor Persistence、Retry UI 與 Rendering。
HTTP/SSE 是選用的假設層
Server 端的 @roackb2/heddle/hosted/http-sse 提供嚴格 Replay Cursor Parsing,以及基於 Node IncomingMessage 與 ServerResponse 的 SSE Streaming。它負責 Canonical Frame、Backpressure 與只清理 Subscriber 的 Disconnect Cleanup;不會註冊 Route,也不會決定 Auth、CORS、Rate Limit、Validation 或 JSON Error Policy。
Browser 端的 @roackb2/heddle-remote/http-sse 加入 Conventional REST Run Resource、已驗證的 fetch、Response Validation、Incremental SSE Parsing 與 Event Identity Check。它不負責 React、Retry Timer、Auth Policy 或 UI State。
部署含義
一個 Process 必須能把 Start、Subscribe、Cancel 與 Approval Operation 路由到擁有 Active Run 的 ConversationRunService。Single-process Deployment 可以直接呼叫;Multi-instance Deployment 則只在拓樸需要時加入 Sticky/Shared Routing 或 Broker。
不要把 Process-local Replay Buffer 描述為 Durable Delivery。如果產品需要 Restart Recovery 或 Cross-region Continuation,請明確設計該 Infrastructure,並繼續以 Heddle Session Repository 作為 Durable Conversation Source of Truth。
上線前必須驗證的不變條件
- 第一個 Run 仍 Active 時,相同 Address 的第二次 Start 會產生 Conflict。
- 一個 Subscriber 中斷絕不會取消底層 Run。
- 從 Sequence N 重新連線,不會重新啟動 Execution,也不會重複已接受的 Event。
- Sequence Gap 會明確失敗,而不是推進 Cursor。
- 不同 Authenticated Scope 無法只靠猜中 Run ID 進行 Subscribe、Cancel 或 Resolve Approval。
- 即使 Execution 幾乎同時完成,Explicit Cancellation 仍會產生 Terminal Cancelled Event。
- 只能接受一個 Terminal Event,之後的 Event 不可再修改 Client State。
- 只有明確投影的 Public Activity、Result 與 Error Field 能跨越 Transport Boundary。