iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
AI Engineering

GitHub Copilot SDK 實戰:從 Agent 應用到可部署服務系列 第 20

Day 20 - Session 生命週期:初始化、停止判斷與結束處理

  • 分享至 

  • xImage
  •  

使用者提示與工具執行階段的 Hooks,已經讓應用程式可以在 Agent 工作期間整理輸入、控制工具執行,並處理工具完成後的結果。這些介入點大多圍繞某一則輸入或某一次工具呼叫,處理的是 Agent 執行過程中的特定階段。

當一段工作跨越多次互動與多個 Turn,應用程式還需要處理更長範圍的狀態,例如 Session 建立或恢復時要準備哪些共用 Context、Agent 自然準備停止時是否仍有必要工作,以及 Session 真正結束後如何完成紀錄與資源收尾。Copilot SDK 提供對應的生命週期 Hooks,讓這些處理可以放在整段 Session 的開始、停止判斷與結束位置。

Session 生命週期需要處理什麼?

Session 可以持續經過多次互動,每一則訊息又可能包含數個 Turn 與工具執行。有些狀態因此會跨越單一 Prompt 或 Tool Call 存在,例如整段工作共用的背景、應用程式追蹤的狀態,以及最後是否已經符合完成條件。

Session 層級通常需要處理三個不同階段的問題:

  • 初始化工作階段:Session 建立或恢復時,準備整段工作共用的 Context,並初始化應用程式需要持續追蹤的狀態,例如開始時間或完成條件。
  • 確認 Agent 是否可以停止:Agent 自然準備停止時,應用程式可以再確認必要驗證、政策檢查或其他確定性條件是否已經成立。
  • 處理 Session 結束:整段工作階段真正結束後,完成最後紀錄、計算執行時間,並釋放目前 Session 持有的暫存狀態與資源。

這三個階段也代表不同的生命週期邊界。Agent 完成目前工作,不代表 Session 已經結束;Session 仍然可以繼續接收後續訊息。只有當整個工作階段真正進入結束流程時,才需要處理 Session 層級的最後收尾。

Session 生命週期的介入點

前面將 Session 生命週期拆成工作階段初始化、Agent 自然停止與 Session 結束三個階段。應用程式要在這些位置加入自己的處理邏輯,需要由 Agent Runtime 在執行流程到達對應階段時,將目前狀態交回應用程式處理,再根據處理結果繼續後面的流程。

Copilot SDK 提供 onSessionStartonAgentStoponSessionEnd 三個生命週期 Hook,分別對應 Session 開始、Agent 自然準備停止,以及 Session 真正結束。把它們放回一次完整的 Session 執行,可以更清楚看出各個介入點之間的關係:

https://ithelp.ithome.com.tw/upload/images/20260920/20150150Ss50QBZRFu.png

Session 建立或恢復時,Runtime 會先透過 onSessionStart 讓應用程式準備整段工作需要的 Context 與狀態。後續 Agent Loop 持續推進,直到 Agent 自然準備停止,再進入 onAgentStop。如果應用程式仍有必要工作尚未完成,可以要求 Agent 繼續;接受目前的停止狀態後,Session 才會進入 session.idle

session.idle 代表目前這一輪 Agent 工作已經停止,Session 本身仍然存在,也可以繼續接收後續訊息。等應用程式真正結束這段工作階段時,才會進一步觸發 onSessionEnd 完成最後的收尾。

因此,Agent 自然停止、session.idle 與 Session 結束位在不同的生命週期位置。session.abort() 則屬於另一條執行控制路徑,用來中止目前正在處理的訊息,不會經過 onAgentStop 的自然停止判斷。

onSessionStart:準備工作階段

onSessionStart 會在 Session 開始時觸發,應用程式可以利用這個位置準備目前 Session 需要的 Context,或初始化自己要追蹤的 Session 範圍狀態。

建立 Session 時,可以在 hooks 中加入處理函式:

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input, invocation) => {
      console.log(
        `[session:${invocation.sessionId}] start source=${input.source}`,
      );

      return {
        additionalContext:
          "目前正在進行 staging 環境的版本發布準備度審查。",
      };
    },
  },
});

處理函式會取得目前生命週期資訊與 Hook Invocation。以 Node.js SDK 為例,SessionStartHookInput 提供 sourceinitialPrompt,而 invocation.sessionId 則可以辨識目前是哪一段 Session。

其中 source 目前可能是 startupresumenew,可以用來辨識這次 Session 開始的來源。範例另外透過 additionalContext 加入整段工作共用的發布審查背景,讓後續 Agent 執行可以使用這項資訊。

這類 Context 只需要在工作階段開始時準備一次,不必在每一則使用者提示重新組合。

onSessionStart 也可以透過 modifiedConfig 調整 Session 設定。如果目前只需要準備工作背景或應用程式狀態,使用 additionalContext 搭配應用程式自己的狀態即可。

onAgentStop:判斷 Agent 是否可以停止

onAgentStop 會在 Agent 自然到達停止點時執行。此時 Agent 已經準備結束目前工作,但 Session 仍然維持作用中。

如果應用程式接受目前的停止狀態,不需要回傳任何結果。若還有必要工作尚未完成,可以回傳 decision: "block",要求 Agent 繼續處理:

onAgentStop: async (input) => {
  if (input.stopHookActive) {
    return;
  }

  return {
    decision: "block",
    reason:
      "請先納入應用程式的最終驗證結果,再整理最後結論。",
  };
},

Runtime 會將 reason 作為後續使用者訊息排入執行流程,再讓 Agent 繼續工作。後續再次自然準備停止時,stopHookActive 會反映前一次 onAgentStop 已經要求 Agent 繼續,因此範例可以直接放行,避免同一個 Hook 持續回傳 block

這段程式主要用來說明 decisionreasonstopHookActive 之間的關係。正式應用程式仍然需要自己的完成條件,不能只依賴 stopHookActive 決定是否接受停止。例如必要驗證是否完成、政策檢查是否已有結果,或工作要求的狀態是否成立,都應由應用程式使用可以確認的資料判斷。

Runtime 本身也會限制連續的 block 決策,避免 Hook 持續要求 Agent 工作而形成無限制的執行循環。後面的完整範例會進一步加入應用程式自己的最終驗證狀態,示範如何將確定性條件與 Agent 的停止流程接在一起。

NOTE:
onAgentStop 目前處理的是 Top-level Agent 的自然停止;Sub-agent 有自己的停止生命週期,中止與執行錯誤也屬於不同執行路徑。

onSessionEnd:處理 Session 結束

Agent 完成目前工作並進入 session.idle 後,Session 仍然可以接收新的訊息。當整個工作階段真正進入結束流程時,才會觸發 onSessionEnd

應用程式可以在這個位置取得 Session 最後的結束資訊:

onSessionEnd: async (input, invocation) => {
  console.log(
    `[session:${invocation.sessionId}] end reason=${input.reason}`,
  );
},

以 Node.js SDK 為例,SessionEndHookInput 提供 reasonfinalMessageerror。其中 reason 描述 Session 最後進入結束流程的原因,目前可能包含以下值:

  • complete:Session 正常完成。
  • error:Session 因錯誤進入結束流程。
  • abort:Session 因中止進入結束流程。
  • timeout:Session 因逾時進入結束流程。
  • user_exit:使用者主動結束 Session。

finalMessage 可以取得結束時的最後訊息,error 則提供結束流程相關的錯誤資訊。應用程式可以依照實際需要使用這些資料完成最後紀錄、更新自己的工作狀態,或釋放目前 Session 持有的資源。

其中 aborttimeout 是 Session 結束原因,不能直接等同 session.abort()sendAndWait() 的逾時。session.abort() 控制的是目前正在處理的訊息;sendAndWait() 的逾時則只限制應用程式願意等待 session.idle 多久。

這個位置處理的是整段 Session 的最後收尾。實際資源如果還具有自己的生命週期,仍然需要由應用程式負責正確釋放,而不能只依賴 Hook 本身完成所有清理工作。

執行錯誤的介入點

前面的三個 Hook 對應 Session 開始、Agent 自然停止與 Session 結束。Agent 工作持續進行時,如果模型呼叫、工具執行、系統處理或使用者輸入發生錯誤,則屬於另一條執行錯誤處理路徑。

Copilot SDK 提供 onErrorOccurred,讓應用程式在 Session 執行發生錯誤時取得錯誤內容與發生位置,再依照錯誤性質決定是否需要改變 Runtime 原本的處理方式:

const session = await client.createSession({
  hooks: {
    onErrorOccurred: async (input, invocation) => {
      console.error(
        `[session:${invocation.sessionId}] ` +
          `error context=${input.errorContext} recoverable=${input.recoverable}`,
      );

      if (
        input.errorContext === "model_call" &&
        input.recoverable
      ) {
        return {
          errorHandling: "retry",
          retryCount: 2,
        };
      }

      return;
    },
  },
});

errorContext 用來辨識錯誤發生的位置,目前可能是 model_calltool_executionsystemuser_inputrecoverable 則表示這次錯誤是否具有恢復的可能性。

需要介入時,應用程式可以透過 errorHandling 指定後續處理方式:

  • retry:重新嘗試目前操作,可以搭配 retryCount 限制重試次數。
  • skip:略過目前發生錯誤的操作,繼續後續處理。
  • abort:中止目前執行。

如果不需要改變 Runtime 原本的錯誤處理,可以不回傳結果。onErrorOccurred 也可以透過 userNotification 提供要呈現給使用者的訊息,或使用 suppressOutput 控制錯誤輸出是否顯示。

錯誤是否適合重試仍然需要根據實際情境判斷。recoverable 提供 Runtime 對這次錯誤是否可能恢復的資訊,但應用程式仍應考慮操作本身是否具備安全重試條件。例如已經產生外部副作用的工具操作,不能只因錯誤被標示為可恢復,就直接假設可以再次執行。

onErrorOccurredonAgentStop 處理的是不同情況。onAgentStop 發生在 Top-level Agent 自然準備停止時;onErrorOccurred 則處理 Session 執行期間發生的錯誤。如果錯誤最後使整個 Session 進入結束流程,再由 onSessionEndreason 描述最後的結束原因。

實作:建立 Session 生命週期控制流程

接下來建立一個版本發布準備度審查範例,把 Session 初始化、Agent 自然停止判斷與 Session 結束放進同一段執行流程。

範例使用固定的版本狀態,不讀取程式碼庫、不呼叫外部服務,也不執行真正的部署。Session 開始時,應用程式會加入版本資訊並建立生命週期狀態;Agent 第一次自然準備停止時,再執行固定的最終發布檢查,要求 Agent 將結果納入最後回答。等工作完成並結束 Session 後,onSessionEnd 負責記錄工作時間並清除生命週期狀態。

準備專案環境

先建立 Node.js 專案並啟用 ES Modules:

$ mkdir copilot-sdk-session-lifecycle
$ cd copilot-sdk-session-lifecycle
$ npm init -y --init-type module
$ mkdir src

安裝 Copilot SDK 與 TypeScript 執行環境:

$ npm install @github/copilot-sdk
$ npm install --save-dev @types/node typescript tsx

這個範例不使用 Tool,版本資料也全部保存在程式中,不需要另外準備資料庫或其他服務。

建立 Session 生命週期控制程式

建立 src/index.ts

import { CopilotClient } from "@github/copilot-sdk";

type FinalValidation = {
  approved: boolean;
  summary: string;
};

type LifecycleState = {
  startedAt: Date;
  finalValidation?: FinalValidation;
};

const releaseStatus = {
  version: "2026.08.23",
  targetEnvironment: "staging",
  tests: {
    passed: 128,
    failed: 0,
  },
  migrationReady: true,
  rollbackAvailable: true,
  risk: {
    description: "Refresh Token 輪替尚未啟用。",
    acceptedForTarget: true,
    requirement: "production 前必須完成。",
  },
};

const lifecycleStates = new Map<string, LifecycleState>();

function buildReleaseContext(): string {
  return [
    `Release: ${releaseStatus.version}`,
    `Target environment: ${releaseStatus.targetEnvironment}`,
    `Automated tests: ${releaseStatus.tests.passed} passed, ${releaseStatus.tests.failed} failed`,
    `Migration ready: ${releaseStatus.migrationReady}`,
    `Rollback available: ${releaseStatus.rollbackAvailable}`,
    `Known risk: ${releaseStatus.risk.description}`,
    `Risk accepted for target: ${releaseStatus.risk.acceptedForTarget}`,
    `Risk requirement: ${releaseStatus.risk.requirement}`,
  ].join("\n");
}

function runFinalValidation(): FinalValidation {
  const approved =
    releaseStatus.tests.failed === 0 &&
    releaseStatus.migrationReady &&
    releaseStatus.rollbackAvailable &&
    releaseStatus.risk.acceptedForTarget;

  return {
    approved,
    summary: approved
      ? `staging 可以進行;${releaseStatus.risk.description} ${releaseStatus.risk.requirement}`
      : "目前仍有條件阻擋 staging 發布。",
  };
}

let resolveSessionEnd!: () => void;

const sessionEndCompleted = new Promise<void>((resolve) => {
  resolveSessionEnd = resolve;
});

const client = new CopilotClient();

const session = await client.createSession({
  model: "auto",
  availableTools: [],
  hooks: {
    onSessionStart: async (input, invocation) => {
      lifecycleStates.set(invocation.sessionId, {
        startedAt: input.timestamp,
      });

      console.log(
        `[session:${invocation.sessionId}] start source=${input.source}`,
      );

      return {
        additionalContext: [
          "目前正在進行版本發布準備度審查。",
          buildReleaseContext(),
          "請先根據目前資料形成判斷。",
          "如果應用程式在停止前提供最終驗證結果,請將它納入最後結論。",
        ].join("\n"),
      };
    },

    onAgentStop: async (input, invocation) => {
      const state = lifecycleStates.get(invocation.sessionId);

      if (!state) return;

      console.log(
        `[session:${invocation.sessionId}] ` +
          `agent stop reason=${input.stopReason ?? "unknown"} ` +
          `active=${input.stopHookActive === true}`,
      );

      if (input.stopHookActive || state.finalValidation) return;

      const finalValidation = runFinalValidation();
      state.finalValidation = finalValidation;

      console.log(
        `[session:${invocation.sessionId}] ` +
          `final validation approved=${finalValidation.approved}`,
      );

      return {
        decision: "block",
        reason: [
          "應用程式已完成最終發布檢查。",
          `結果:${finalValidation.approved ? "通過" : "阻擋"}`,
          `說明:${finalValidation.summary}`,
          "請將這項結果納入最後回答,再結束目前工作。",
        ].join("\n"),
      };
    },

    onSessionEnd: async (input, invocation) => {
      const state = lifecycleStates.get(invocation.sessionId);
      const durationMs = state
        ? input.timestamp.getTime() - state.startedAt.getTime()
        : undefined;

      console.log(
        `[session:${invocation.sessionId}] ` +
          `end reason=${input.reason} ` +
          `durationMs=${durationMs ?? "unknown"}`,
      );

      lifecycleStates.delete(invocation.sessionId);
      resolveSessionEnd();
    },
  },
});

const response = await session.sendAndWait(
  {
    prompt:
      "請判斷目前版本是否適合進入 staging,整理主要依據與仍需注意的風險。",
  },
  120_000,
);

console.log("\n模型回應:");
console.log(response?.data.content);

await session.disconnect();
await sessionEndCompleted;
await client.stop();

範例程式把三個生命週期 Hook 與應用程式狀態放進同一段 Session。接下來依照實際執行順序,拆解每個階段負責的工作。

初始化 Session 狀態

Session 開始時,程式先建立目前工作階段使用的生命週期狀態:

lifecycleStates.set(invocation.sessionId, {
  startedAt: input.timestamp,
});

lifecycleStates 是應用程式自己的狀態。範例先保存開始時間,後續 onAgentStoponSessionEnd 都可以根據相同的 Session ID 取得目前工作階段的資料。

接著透過 additionalContext 加入版本狀態:

return {
  additionalContext: [
    "目前正在進行版本發布準備度審查。",
    buildReleaseContext(),
    "請先根據目前資料形成判斷。",
    "如果應用程式在停止前提供最終驗證結果,請將它納入最後結論。",
  ].join("\n"),
};

這些資料是整段版本發布審查共同使用的背景,因此在 Session 開始時準備一次即可,不需要把相同資訊重複放進後續每一則使用者提示。

範例另外使用 input.timestamp 記錄開始時間。以 Node.js SDK 為例,Hook 公開介面的 timestamp 會以 Date 提供,因此後面可以直接用來計算執行時間。

這裡的 Map 只用於保存範例中的生命週期狀態。正式服務如果需要讓工作跨越應用程式程序生命週期,或由多個應用程式執行個體共同處理,就不能只依賴程序內記憶體。

檢查 Agent 是否可以停止

Agent 根據目前 Context 完成處理並自然準備停止時,onAgentStop 會先取得目前 Session 的應用程式狀態:

const state = lifecycleStates.get(invocation.sessionId);

if (!state) return;

接著確認是否已經進入前一次 block 所形成的續行流程,或應用程式自己的最終驗證是否已經完成:

if (input.stopHookActive || state.finalValidation) return;

第一次進入 onAgentStop 時,最終驗證尚未執行,因此程式會呼叫:

const finalValidation = runFinalValidation();
state.finalValidation = finalValidation;

runFinalValidation() 是一般 TypeScript 程式邏輯,根據固定的版本狀態檢查測試、Migration、Rollback 與目前風險是否符合 staging 的條件:

const approved =
  releaseStatus.tests.failed === 0 &&
  releaseStatus.migrationReady &&
  releaseStatus.rollbackAvailable &&
  releaseStatus.risk.acceptedForTarget;

這項結果由應用程式確定,不需要讓 Agent 自行判斷最終驗證是否已經完成。

完成檢查後,再透過 onAgentStop 要求 Agent 把驗證結果納入後續處理:

return {
  decision: "block",
  reason: [
    "應用程式已完成最終發布檢查。",
    `結果:${finalValidation.approved ? "通過" : "阻擋"}`,
    `說明:${finalValidation.summary}`,
    "請將這項結果納入最後回答,再結束目前工作。",
  ].join("\n"),
};

Runtime 會將這段 reason 作為後續使用者訊息排入執行流程,再讓 Agent 繼續工作。後續再次自然準備停止時,應用程式狀態已經保存 finalValidationstopHookActive 也會反映目前的續行狀態,因此 Hook 可以正常放行。

這裡由應用程式掌握確定性的完成條件,reason 則負責把還需要處理的內容帶回 Agent。兩者分開後,就不需要依賴模型自行宣稱某項驗證是否已經完成。

處理 Session 結束與狀態清理

Agent 完成後續處理並再次自然停止,sendAndWait() 會等到 Session 進入 session.idle 再結束等待。此時目前的 Agent 工作已經停止,但 Session 本身仍然存在。

範例完成目前工作後,主動呼叫:

await session.disconnect();

以 Node.js SDK 為例,session.disconnect() 會結束目前 SDK 對這個 Session 的使用,並釋放相關記憶體資源;磁碟上保存的 Session 狀態仍可供後續恢復。永久刪除 Session 資料則需要另外呼叫 client.deleteSession()

Session 結束時,onSessionEnd 先取得一開始保存的生命週期狀態,再利用 Hook 的時間戳記計算這段工作的執行時間:

const durationMs = state
  ? input.timestamp.getTime() - state.startedAt.getTime()
  : undefined;

開始與結束時間都來自生命週期 Hook,因此這裡計算的是兩個事件時間點之間的差值,不會混入 onSessionEnd Handler 實際開始執行的時間。

完成最後紀錄後,再移除應用程式保存的狀態:

lifecycleStates.delete(invocation.sessionId);

如果 Session 執行期間還建立暫存檔、鎖定、租約或其他應用程式資源,也應由實際持有這些資源的程式負責釋放。

範例另外建立 sessionEndCompleted,讓主流程可以確認 onSessionEnd 已經完成:

const sessionEndCompleted = new Promise<void>((resolve) => {
  resolveSessionEnd = resolve;
});

onSessionEnd 完成後會呼叫 resolveSessionEnd(),因此主流程可以在 disconnect() 後等待清理完成,再停止 Client:

await session.disconnect();
await sessionEndCompleted;
await client.stop();

這樣不需要加入固定延遲時間猜測 Hook 是否已經執行,也能讓範例中的 Session 結束流程完整收尾。

執行應用程式

完成程式後執行:

$ npx tsx src/index.ts

實際自然語言回答與 Turn 數量會受到模型執行情況影響,不需要期待固定結果。執行時主要確認生命週期是否依照預期推進。

終端機可能看到類似:

[session:<session-id>] start source=new
[session:<session-id>] agent stop reason=<stop-reason> active=false
[session:<session-id>] final validation approved=true
[session:<session-id>] agent stop reason=<stop-reason> active=true

模型回應:
...

[session:<session-id>] end reason=<end-reason> durationMs=<duration>

第一次 Agent 自然準備停止時,應用程式會執行最終驗證並要求 Agent 繼續;後續再次停止時,驗證狀態已經存在,因此可以正常結束目前工作。

sendAndWait() 等待的是 session.idle,所以模型回應會在目前 Agent 工作停止後回到應用程式。接著呼叫 disconnect(),才會進一步進入 Session 結束與收尾流程。

Session 生命週期控制的適用情境與限制

前面的範例把三個生命週期 Hook 放進同一段 Session,讓應用程式可以在工作階段開始、Agent 自然停止與 Session 結束時加入自己的處理邏輯。實際使用時,這些介入點仍然只是執行流程的一部分;初始化需要多久、什麼條件才算完成、異常時如何恢復,以及資源如何確實清理,仍然要由應用程式自行管理。

幾個和正式使用直接相關的限制與注意事項,可以整理如下:

  • Session 開始時的處理onSessionStart 應保持輕量,適合建立應用程式狀態、載入必要 Context 或準備基本執行設定。如果初始化需要等待大量外部工作,Session 開始流程也會跟著受到影響。
  • Agent 自然停止onAgentStop 需要搭配明確的完成條件使用。decision: "block" 可以要求 Agent 繼續處理,但不適合作為無限制重試機制;應用程式仍需要知道什麼條件成立後可以接受停止。
  • 執行錯誤與恢復onErrorOccurred 可以依照錯誤位置與恢復條件調整後續行為,但即使錯誤具有恢復可能性,也不能忽略 Tool 或外部服務已經完成的副作用;是否重試仍應由應用程式根據實際操作語意判斷。
  • Session 結束時的清理onSessionEnd 適合完成最後紀錄與資源收尾,但清理流程仍應具備冪等性。程序異常終止時,不能假設 Hook 一定有機會完成,重要資源仍需要其他恢復或逾時清理機制。
  • Session 結束與資料保存:屬於不同責任。結束目前 Session 的使用,不代表已保存的 Session 資料同時被永久刪除;需要保存、恢復或刪除哪些資料,仍應依照實際 Session 生命週期與持久化設計處理。

生命週期與錯誤處理 Hook 提供明確的介入位置,應用程式本身的狀態管理、完成條件、錯誤恢復與資源生命週期仍然由應用程式負責。

小結

Session 生命週期涵蓋工作階段開始、Agent 自然停止與 Session 結束等不同位置;執行期間的錯誤則有獨立的介入路徑。這些 Hook 讓應用程式可以在 Agent 工作之外加入自己的初始化、完成條件、錯誤處理與資源收尾邏輯:

  • Session 開始時,可以透過 onSessionStart 準備整段工作共用的 Context,並初始化應用程式需要追蹤的 Session 範圍狀態。
  • Agent 自然準備停止時,onAgentStop 讓應用程式再確認必要完成條件是否成立;仍有工作需要處理時,可以要求 Agent 繼續。
  • Session 執行發生錯誤時,onErrorOccurred 可以根據錯誤位置與恢復條件,決定是否重試、略過或中止目前處理。
  • Session 真正結束時,onSessionEnd 可以取得最後的結束資訊,用來完成紀錄、更新狀態與釋放目前工作持有的資源。

這些介入點分別處理工作開始、自然停止、執行錯誤與最終收尾,不同狀態之間也有各自的生命週期邊界。區分這些責任後,Agent Runtime 可以持續負責推進 Agent 工作,應用程式則保留對狀態初始化、完成條件、錯誤恢復與資源生命週期的控制。


上一篇
Day 19 - 工具執行後處理:結果轉換與失敗引導
下一篇
Day 21 - GitHub 認證:使用者身分與 Server-to-Server 存取
系列文
GitHub Copilot SDK 實戰:從 Agent 應用到可部署服務22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言