概念 · 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 投影。
Heddle 負責
  • 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:

TypeScript
await engine.turns.submit({ sessionId, prompt, host })

如果 Caller 會保持連線直到完成,這已經足夠。當 Turn 必須脫離單一 Request 被獨立定址時,再加入 ConversationRunService。例如 HTTP Request 要立刻回傳、SSE Subscriber 稍後才連線、User 要從另一個 Request 取消,或 Approval 必須等待另一個 UI Action。

TypeScript
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:

TypeScript
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

TypeScript
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 包含 runIdacceptedAtresult Promise、Async events(...) Stream、cancel()resolveApproval(...)

如果 Host 必須在 Execution 開始前持久記錄或稽核已接受的 Run,請使用 Acceptance Hook。Async startAndWaitForAcceptance 路徑只會在該 Hook 成功後回報 Acceptance。

標準事件封套

每個 Published Item 都使用相同 Envelope:

TypeScript
{
  runId: string
  sequence: number
  timestamp: string
  kind: 'activity' | 'result' | 'cancelled' | 'error'
}

Payload 依 kind 而不同:

  • activity 帶有一個 Semantic ConversationActivity
  • result 帶有最終 Result 或 Host 投影後的 Result;
  • cancelled 帶有取消原因;
  • error 帶有 Public { code, message } Projection。

Sequence 從 1 開始,並在單一 Run 內逐一遞增。一個 Run 可以發出多個 Activity Event,但只能有一個 Terminal Event:resultcancellederror。Terminal Settlement 後 Subscriber 會關閉,之後的 Event 都是無效的。

比較 Activity 時,請使用 HeddleEventType,不要複製 assistant.streamtool.callingtool.completedtool.approval_requestedcompaction.running 等 String Literal。

Replay 與重新連線

Subscriber 重新連線時,提供最後已接受的 Sequence:

TypeScript
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。對 startTurnstartContinue 而言,已要求取消後,即使 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 前,使用 projectResultprojectError

Remote Client 應加入使用同步 Standard Schema Validator 的 ConversationRunProtocolCodec。Validator Output 就是 Public Allowlist。Codec 會在 Parse 或 Serialize 前驗證 Run ID、正整數 Sequence、Timestamp、Terminal Vocabulary、Host Payload 與 JSON Safety。

TypeScript
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,它會:

  1. 提供 Canonical afterSequence Cursor;
  2. 忽略屬於其他 Run 的 Event;
  3. 忽略已接受過的 Duplicate Sequence;
  4. 發現 Sequence Gap 時拋出錯誤;
  5. 只在接受 Event 後推進 Cursor;
  6. 辨識所有三種 Terminal Kind;
  7. 拒絕 Terminal 後收到的 Event;
  8. 計算有界 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 IncomingMessageServerResponse 的 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。

上線前必須驗證的不變條件

  1. 第一個 Run 仍 Active 時,相同 Address 的第二次 Start 會產生 Conflict。
  2. 一個 Subscriber 中斷絕不會取消底層 Run。
  3. 從 Sequence N 重新連線,不會重新啟動 Execution,也不會重複已接受的 Event。
  4. Sequence Gap 會明確失敗,而不是推進 Cursor。
  5. 不同 Authenticated Scope 無法只靠猜中 Run ID 進行 Subscribe、Cancel 或 Resolve Approval。
  6. 即使 Execution 幾乎同時完成,Explicit Cancellation 仍會產生 Terminal Cancelled Event。
  7. 只能接受一個 Terminal Event,之後的 Event 不可再修改 Client State。
  8. 只有明確投影的 Public Activity、Result 與 Error Field 能跨越 Transport Boundary。

權威來源