可執行的託管邊界

讓一個執行服務隨宿主長駐

當 request 與訂閱的生命週期不同、客戶端可能重新連線,或一個回合需要可獨立定址的取消與核准生命週期時,請加入 `ConversationRunService`。

客製化深度

進階生命週期

託管深度

長駐 Node 行程

文件狀態

可執行參考範例

前置假設

  • 單一宿主行程可以在 active run 的整個生命週期內擁有它。
  • 應用程式會先完成身分驗證,再建立執行位址。
  • 你的產品能把後續訂閱、取消與核准操作路由到同一個執行擁有者。
Heddle 負責
  • 每個宿主定義位址只允許一個 active run,並提供穩定 run ID、依序活動、單一終結事件、取消、核准與有界重播。
  • 即使較晚完成的 executor 忽略 abort signal,取消仍具有最終決定權。
  • 在發布終結事件前,等待結果投影完成,並套用安全的對外錯誤投影。
產品端負責
  • 經過驗證的帳號或租戶範圍,以及穩定的對話 ID。
  • 引擎建立、憑證、工具、儲存轉接器、核准決策、遙測與行程生命週期。
  • 每次 retained run 查詢的授權,以及多行程部署中的路由。

安裝與匯入

Hosted surface 明確表示伺服器行程的託管條件,但不會加入任何 HTTP 框架。

Shell
npm install @roackb2/heddle
TypeScript
import { ConversationRunCancelledError, createConversationEngine } from '@roackb2/heddle'
import { ConversationRunService } from '@roackb2/heddle/hosted'

只建立一次

在與伺服器或 worker 相同的生命週期邊界建立一個 service。位址可以包含產品範圍,但一定要包含穩定的 sessionId

TypeScript
type ProductRunAddress = {
  accountId: string
  sessionId: string
}

const runs = new ConversationRunService<ProductRunAddress>({
  addressKey: ({ accountId, sessionId }) =>
    JSON.stringify([accountId, sessionId]),
  replay: { maxEventsPerRun: 512, retentionMs: 300_000 },
})

不要在每個 request 中各建立一個 service。這會丟失 active-run discovery 與重新連線重播,也會讓不同 request 對目前 active run 有不同答案。

啟動持久回合

先透過引擎取得或建立 Heddle session,再啟動 run。請使用穩定的產品對話 ID,讓第二個 prompt 延續同一段對話。

TypeScript
const engine = createConversationEngine({
  workspaceRoot,
  stateRoot,
  model: 'gpt-5.4',
})

const session =
  (await engine.sessions.readExisting(productConversationId)) ??
  (await engine.sessions.create({ id: productConversationId }))

const run = runs.startTurn({
  address: { accountId, sessionId: session.id },
  engine,
  turn: { sessionId: session.id, prompt, host },
  projectResult: async (result, context) => {
    context.controller.signal.throwIfAborted()
    await applyAuthorizedProductResult(result)
    return { outcome: result.outcome, summary: result.summary }
  },
  projectError: () => ({
    code: 'agent_failed',
    message: 'The agent could not complete this request.',
  }),
})

return { accepted: true, runId: run.runId, acceptedAt: run.acceptedAt }

projectResult 是一個 transaction boundary。Heddle 會等它完成才發布成功終結事件,因此產品持久化或 reconciliation 不會晚於使用者看見的成功結果。若投影失敗,run 也會失敗。projectError 是對稱的安全邊界:宿主保留原始 failure 供診斷,訂閱端只收到核准過的 code 與 message。

訂閱、重播與取消

TypeScript
for await (const event of run.events({ afterSequence, signal })) {
  await sendToSubscriber(event)
}

const result = await run.result

Cancellation 是另一條終結路徑。cancel() 具有最終決定權,result promise 會以 ConversationRunCancelledError reject:

TypeScript
run.cancel()
try {
  await run.result
} catch (error) {
  if (!(error instanceof ConversationRunCancelledError)) throw error
}

每個事件都有 runId、單調遞增的 sequence 與時間戳記。串流先輸出 activity,最後只會有一個 resultcancellederror 終結事件。中止 run.events(...) 只會中斷該訂閱端,不會取消 run。

Handle 上的 cancel()resolveApproval(...) 永遠綁定那一個確切的 run。過期 handle 無法影響同一位址上的後續 run。

授權 retained handle

後續 request 可以查詢 retained run,但只有 run ID 絕不代表已授權:

TypeScript
const retained = runs.getRetainedRun(runId)
if (!retained || retained.accountId !== authenticatedAccountId) {
  throw new Error('Run not found')
}

對錯誤範圍回傳 not found,可以避免洩漏其他租戶的 run 是否存在。不要只為了授權再維護第二套宿主 run registry;retained handle 已包含原始的宿主定義位址。

必須納入設計的限制

  • 重播與 retained handle 位於記憶體且有上限。預設每個 run 保留 512 個事件,保留時間為五分鐘。
  • 持久的最終對話狀態位於對話 repository,而不是 replay buffer。
  • 此 service 不提供重啟後復原或跨 instance 傳遞。
  • 多行程宿主必須把操作路由到擁有者行程,或加入共享傳遞基礎設施。
  • 範例中的 HostedAgentService 是示範組合方式的應用程式碼,不由 Heddle 匯出,也不應成為產品必須使用的 wrapper。

權威來源