跳轉到

啟動與組裝根(Boot and Config)

python -m workspace_app(外加一份 operator 的 config.yaml)變成一個活著的 FastAPI app 的「組裝根」(composition root):載入型別化的 Settings、記錄每個 leaf 的來源(provenance)、透過 factories.get_* 建出每個可插拔的 Protocol/ABC 實作、敘事化每個 blocking 啟動步驟、裝上觀測(config dump + 可重播的 litellm 呼叫紀錄),最後把全部東西注入 create_app

看這篇之前:先讀 架構總覽 抓全貌。

職責與邊界

這個子系統是唯一決定「哪個實作墊在每個接縫底下」的地方 —— 換一個 backend 是改 config,不是改 code。它負責:

  • 設定載入:bundled defaults ◇ operator YAML 的層疊合併、${VAR} 環境變數插值、嚴格驗證、per-leaf provenance。
  • 接縫選擇factories.get_*(settings) -> Protocol impl,把 sandbox / filestore / runner / embedder / chunker / ILlm / IVlm 等每個 seam 對應到一個具體實作。
  • 啟動敘事與觀測boot_step 包住每個 blocking 步驟;emit_config_dump 印出已解析設定;install_llm_logging 裝上忠實可重播的 LLM 呼叫紀錄。
  • 注入 create_app:把 ~40 個 keyword seam 餵進 FastAPI 組裝。

負責的事:

  • 不負責 FastAPI 內部的 route 註冊、回合引擎、sweeper 排程等 —— route 註冊散在各 register_*_routes 模組(meta_routes.py/item_routes.py/chat_routes.py/…,#54),lifespan 與 sweeper 在 api/lifecycle.py:build_lifespan,細節見 API 與回合引擎
  • 不負責背景工作的實際消費邏輯 —— 它只建出 coordinator 物件並(在 run_consumers 為真時)啟動消費,細節見 背景工作與擴展
  • 不負責 specstar 的資料模型本身 —— 那是 資料層(specstar);這裡只組出 BackendConfig 並呼叫 make_spec
  • 測試factories.get_*:測試直接 Settings(...) + 注入 mock。factory 只服務 production 組裝。

核心模組

路徑 角色
src/workspace_app/__main__.py Production entrypoint / thin composition root。main():解析 --configload_with_provenance、印出套用的 config 檔、emit_config_dump、定義 get_user_id 接縫、install_llm_logging,然後一連串 boot_step(...) 包住的 factory 呼叫,最後 create_app(...) + uvicorn.run
src/workspace_app/config/schema.py 型別化的巢狀 Settings frozen-dataclass 樹 + bundled defaults。各 section:server/sandbox/sandbox_host/tools/filestore/runner/llm/read_file/exec/history/kb/agents/health/message_queue/observability/failover。ServerSettingsdefault_user/superusers/run_consumers
src/workspace_app/config/loader.py 載入 pipeline orchestrator。load_with_provenance 把路徑解析→YAML 解析→插值→層疊合併→嚴格 _validate_settings_from_dict_build_provenance 串起來。定義 Source/ProvenanceSOURCE_CONFIG/ENV/DEFAULT
src/workspace_app/config/interpolate.py ${NAME}/$NAME 環境變數插值。expand_env 對未設定的引用丟 EnvVarUnset(fail-loud);$$$has_env_reference 驅動 provenance 的 env-vs-literal 標記。
src/workspace_app/config/merge.py merge_layered(base, override) —— loader(defaults ◇ YAML)與 catalog(preset ◇ usage)共用的 deep merge。dict 遞迴、list 整段取代、其餘 override 勝出。
src/workspace_app/config/catalog_build.py build_catalog(settings, config_dir) -> AgentConfigCatalog。逐一走 agents.sub_agents 的每個 purpose,resolve_usage 把 usage ◇ preset、解析 prompt_file→body、建出 AgentConfig。KB-required purpose 為空或沒給 kb_search 就 fail-loud。
src/workspace_app/config/prompt_file.py resolve_prompt_file(value, config_dir) —— 把 preset 的 prompt_filepkg:/絕對/相對)解析成 markdown body;相對路徑以 config_dir 為錨。
src/workspace_app/config/dump.py 觀測功能 A。emit_config_dump 把已解析的樹印成註解 YAML key: value # ← <source>(stdout 上遮罩 secret)並寫一份真值 config.resolved.yaml(0600)。best-effort,絕不擋 boot。
src/workspace_app/factories.py 接縫選擇層:get_*(settings) -> Protocol impl。包含 get_spec/_backend_forget_sandboxget_filestoreget_runner,以及所有 LLM 角色 factory(透過 resolve_llm_chain + LlmEndpoint + _llm_from_chain/_vlm_from_chain)、parser/check registry、build_message_queue_factory
src/workspace_app/api/app.py create_app(*, spec, sandbox, filestore, runner, ...~40 seams, run_consumers=True) —— 精簡組裝根(~767 行,#54)。組服務 → 建 deep module(ItemLocator/TurnContextBuilder/ChatSendService/SubagentBridge/WorkflowExecutor/ReplayLoaders/MentionService)→ 逐一呼叫各 register_*_routes 掛 route → build_lifespan → mount SPA;不再自己定義 route handler。用 coordinators.build_coordinators 建 coordinator。
src/workspace_app/api/lifecycle.py build_lifespan(...) —— FastAPI lifespan factory(#54 由 app.py 抽出)。boot_step 跑 health-check 與 seed help collectionseed_help_collection_best_effortasyncio.to_thread 離開 event loop,best-effort,id 存進 app.state.help_collection_id,#230),只在 run_consumers 為真時對 wiki/index/sanity/card_gen 呼叫 start_consuming(),並起五個背景 sweeper(idle_killer / mirror_sweeper / index_sweeper #227 / code_sync_sweeper / blob_gc_sweeper #245)。
src/workspace_app/coordinators.py FastAPI-free 組裝根 build_coordinators(+ build_ingestor/resolve_wiki_config),被 create_app 與 worker entrypoint 共用,讓 API 能當 pure producer 而 worker pod 各消費一個 JobType。也從 kb.wiki.llm preset 建出可選的 code_wiki_llm(wiki_model 為空 → None → code-wiki 關閉,build 會在 WikiBuildState 記下 misconfig,#281);code_card_batch_chars 是 coordinator 建構子預設(24k chars),非 config key。
src/workspace_app/worker/__main__.py 獨立 worker entrypoint python -m workspace_app.worker <jobtype> —— 用同一個 build_coordinators block-consume 一個 JobType,是 run_consumers 拆分的另一半。
src/workspace_app/observability/boot.py boot_step(label) context manager —— → enter / ✓ done (1.2s) / ✗ failed 敘事,每行 flush,讓卡住的步驟自己報名(#208)。
src/workspace_app/observability/setup.py install_llm_logging(settings) —— gate(WORKSPACE_LLM_LOG env 覆蓋 config observability.llm_log.enabled,預設 on)後構造 LlmCallLogger(LlmLogWriter(dir)) 並 append 到 litellm.callbacks
src/workspace_app/observability/logger.py LlmCallLogger(CustomLogger) —— 對每個 litellm 呼叫觸發;async success/failure hook 建 record 並用 asyncio.to_thread 寫出(不卡),全部 best-effort(不錯)。
src/workspace_app/observability/record.py 純函式,從 litellm kwargs+response 建出忠實、可貼上重播的 record:request block 等同 litellm.completion(**request) kwargs。classify_call 分桶 generative/embedding/rerank/other。
src/workspace_app/observability/writer.py LlmLogWriter —— 日期分區的磁碟 layout(<root>/<day>/NNNN-<call_type>-<id>.json + index.jsonl),並丟一支 replay.py 助手重播任一 record。同步,經 to_thread 離開 event loop 呼叫。

介面與接縫

factories.get_* 是這些 seam 的唯一選擇點;下游只依賴 Protocol/ABC,不依賴具體實作或 Settings

Seam 種類 定義位置 實作(正式 / 測試)
Sandbox Protocol src/workspace_app/sandbox/protocol.py LocalProcessSandboxHttpSandboxDockerSandbox(已棄用)/ MockSandbox
FileStore Protocol src/workspace_app/filestore/protocol.py SpecstarFileStore / MemoryFileStore
AgentRunner Protocol src/workspace_app/api/runner.py LitellmAgentRunner / ScriptedAgentRunner
Embedder Protocol src/workspace_app/kb/embedder.py LitellmEmbedder / HashEmbedder
ILlm interface src/workspace_app/kb/llm.py LitellmLlmFallbackLlmsrc/workspace_app/failover/llm.py
IVlm interface src/workspace_app/kb/vlm/protocol.py LitellmVlmFallbackVlm
Chunker Protocol src/workspace_app/kb/chunker.py FixedTokenChunker
MessageQueueFactory interface specstar.message_queue SimpleMessageQueueFactory / RabbitMQMessageQueueFactory
IParser / ISanityCheck ABC/Protocol src/workspace_app/kb/parserssrc/workspace_app/health bundled + dotted-path 自訂

LLM 角色(get_kb_llm/get_card_drafter_llm/get_kb_quality_judge_llm/get_sanity_judge_llm/get_kb_vlm/get_designed_pptx_vlm 等)都走同一條 cascade:resolve_llm_chain(settings, ref) 解出一串 LlmEndpoint,再由 _llm_from_chain/_vlm_from_chain 收斂:0 個 → None(角色關閉)、1 個 → 純 LitellmLlm/LitellmVlm≥2 個 → busy-aware FallbackLlm/FallbackVlm

運作方式 / 資料流

flowchart TD
  CLI["python -m workspace_app<br/>__main__.main()"] --> LOAD["load_with_provenance()<br/>resolve path → parse YAML →<br/>expand_env(${VAR}) → merge defaults →<br/>_validate → Settings + Provenance"]
  LOAD --> DUMP["emit_config_dump<br/>stdout (masked) +<br/>config.resolved.yaml (0600)"]
  LOAD --> LLMLOG["install_llm_logging<br/>→ litellm.callbacks += LlmCallLogger"]
  LOAD --> FACT["factories.get_*(settings)<br/>(每步 boot_step 敘事)"]
  FACT --> SPEC["get_spec → make_spec<br/>(_backend_for: disk/pg + connect_timeout)"]
  FACT --> SB["get_sandbox / get_filestore / get_runner"]
  FACT --> LLM["LLM 角色<br/>resolve_llm_chain → LlmEndpoint[]<br/>→ LitellmLlm | FallbackLlm/Vlm"]
  FACT --> CAT["get_agent_config_catalog → build_catalog<br/>get_parser_registry / get_check_registry"]
  SPEC --> APP
  SB --> APP
  LLM --> APP
  CAT --> APP["create_app(**seams,<br/>run_consumers=settings.server.run_consumers)"]
  APP -->|run_consumers true| CONS["build_coordinators →<br/>start_consuming (wiki/index/sanity/card_gen)"]
  APP -->|run_consumers false| PROD["pure producer<br/>(worker pods consume)"]
  APP --> UVI["uvicorn.run(app, host, port)"]
  WORKER["python -m workspace_app.worker &lt;jobtype&gt;"] -.共用.-> CB["build_coordinators"]
  CONS --> CB

主要 runtime 路徑(main()):

  1. _parse_args--config / -c
  2. load_with_provenance(config_path) 跑載入 pipeline:解析 config 路徑(顯式 > $WORKSPACE_APP_CONFIG > ./config.yaml);解析 YAML 並保留原始樹供 provenance;_walk_strings 對每個字串 leaf 套 expand_env${VAR} 變活值,未設定 → EnvVarUnset);層疊合併到 asdict(Settings())(把 agents.sub_agents flatten 讓 operator 的扁平 key 能覆蓋,合併後再 pack 回去);嚴格 _validate 對照 _TOP_SCHEMA + preset/fallback/ref/max_searches 檢查;_settings_from_dict 構造型別化 Settings;_build_provenance 從原始 operator YAML 標每個 leaf 為 config.yaml/env/default。
  3. 印出套用了哪個 config 檔,然後 emit_config_dump 把遮罩過的註解 YAML 印到 stdout,並寫真值 config.resolved.yaml(0600)。
  4. get_user_id = lambda: settings.server.default_user —— 同一個接縫穿進 get_spec(讓 specstar 蓋 created_by)與 create_app(access 層 + KB doc-id),兩者不可分歧。
  5. install_llm_logging(settings)WORKSPACE_LLM_LOG/config gate,把 LlmCallLogger append 到 litellm.callbacks
  6. 一連串 boot_step(...) 包住的 factory 呼叫:get_spec(從 filestore 設定經 _backend_for 組 specstar backend,Postgres DSN 注入 connect_timeout;make_spec 註冊所有 model)、可選的 migrate_inline_to_binary(#219,僅 specstar filestore)、discover_packages 工具套件、get_embedder 與各 LLM 角色、get_sandbox。每個 LLM 角色走 resolve_llm_chain + _llm_from_chain 收斂。
  7. create_app(spec=, sandbox=, filestore=get_filestore(settings, spec), runner=get_runner(settings), ...~40 seams, run_consumers=settings.server.run_consumers) 組出 FastAPI,用 build_coordinators 建 coordinator,只在 run_consumers 為真時啟動 in-process 消費。
  8. uvicorn.run(app, host, port)

FastAPI startup 時 build_lifespanapi/lifecycle.py,#54 由 app.py 抽出)接手非同步的 boot 尾段:boot_step 跑 health-check;run_consumers 為真時對 wiki/index/sanity/card_gen 啟動 start_consuming();接著 seed help collection boot_step —— 經 asyncio.to_threadseed_help_collection_best_effort(離開 event loop、best-effort,embedder 掛掉只留下可讀但未索引的 collection,絕不擋 boot),把 id 存進 app.state.help_collection_id(沿用 _ensure_insights_collection 的 by-name 冪等 seed 慣例,#230);最後起五個背景 sweeper。

Worker 路徑(python -m workspace_app.worker <jobtype>)重用 load_with_provenance + build_coordinators,當 API 當 pure producer 時 block-consume 單一 JobType。

關鍵不變式與眉角

Secret 紀律與遮罩

永遠不要讀 configs/config.yaml(活的 secret) —— 只讀 config.example.yamlconfig/schema.py。dump 在 stdout 上遮罩 api_key/pg_dsn/*_token/rabbitmq.url;base_url 刻意遮罩(operator 想看打去哪)。真值只寫進 config.resolved.yaml(0600)。

${VAR} 是唯一的環境覆蓋機制

沒有 per-field 1:1 的 env 查找。未設定的引用在 load 時丟 EnvVarUnset(fail-loud),絕不靜默變空字串(一個 typo 例如 ${OPENAPI_KEY} 不會悄悄關掉認證)。$$→字面 $;FOO=""(設了但空)會代入空字串且報錯。

Provenance 答的是『我設了什麼』

Provenance 由插值前的原始 YAML 推導(operator 實際寫了哪些 leaf),與寫的值是否剛好等於 default 無關。

驗證在啟動時 fail-loud,不是首次使用時

未知 key、壞掉的 preset 引用、自我參照/未知的 fallback、缺 model、壞的 retrieval-llm ref、kb.max_searches_per_turn(null 或正整數)、以及 kb.max_searches_ceiling(有設時須為正整數,KbSettings 預設 10 —— composer 每則訊息可挑的 kb_search 次數上限,#334)全都在啟動時 raise(後兩者同由 loader._check_max_searches 驗證),訊息帶上出錯的 dotted path + 來源檔。Registry resolver(kb.parsershealth.checks)也是零參數構造 dotted-path 類別,未知 id / 壞路徑 / 錯 base class 在啟動 raise,不是首次上傳/執行時。

get_user_id 是單一接縫,穿進兩處

同一個 callable 必須同時餵 get_speccreate_app;分歧會悄悄破壞非預設使用者的 KB 交叉引用連結(#41)。superusers(frozenset)亦同 —— 必須與 make_spec 和 route 的 authorize 一致(#262)。

每個 blocking 步驟都要包 boot_step

config dump 到活著的 server 之間的每個 blocking 步驟都必須包在 boot_step,卡住時最後一行未配對的 就是元兇(#208)。Postgres-down 的卡住會落在 create_appspec.apply,所以 _backend_for 對 Postgres DSN 注入 connect_timeout 讓它 fail-fast。

LLM 角色 chain 的收斂語意

無 chain → None(角色關閉,優雅降級);單一 endpoint → 純 LitellmLlm(無 failover 機制);≥2 endpoint → busy-aware FallbackLlm/FallbackVlm。內層 per-call num_retries=0,因為 failover loop 自己用「換模型」來重試(#196)。

KB-required purpose 必須給 kb_search

kb_chatinfer_modules 必須非空且授予 kb_search,否則 build_catalog raise —— 沒有它的 sub-agent 會悄悄退化成「我無法存取 KB」式的拒答。

觀測絕不擋回合也絕不卡 loop

LLM 紀錄全 best-effort(失敗只記 debug),檔案 I/O 經 asyncio.to_thread 離開 event loop。request block 必須維持逐字可重播(等同 litellm.completion(**request) kwargs)。

run_consumers 只 gate in-process 消費

run_consumers(預設 true = all-in-one)只控制 in-process 的 queue 消費;pod-split 在 API 上設 false(pure producer)並跑 worker pod。build_coordinatorscreate_app 與 worker 共用的唯一 FastAPI-free 根 —— 新 coordinator 加在那裡,不要 inline 進 create_app(#312)。_backend_fordisk_rootpg_dsn 皆空時回 None(in-memory specstar 預設,讓 Settings() 對測試可用)。

設計決策與出處

決策 理由 出處
設定 = bundled defaults ◇ operator YAML,${VAR} 插值為唯一 env 機制 operator 改 config.yaml 就能換任一接縫,單一可稽核的覆蓋路徑;靜默空字串會把 typo 變成悄悄關認證,只在首次 LLM 呼叫才浮現 config-refactor grill Q1/Q2/Q7;interpolate.py / loader.py docstring
嚴格驗證 + provenance dump + 啟動時 fail-loud registry 設定錯誤要立刻可見並帶上出錯路徑,不要變成數小時後才發現的靜默降級執行 loader._validatefactories.get_parser_registry/get_check_registrydump.py
boot_step 敘事每個 blocking 啟動步驟 pod 以前在 config dump 後靜默卡住,任一步卡住看起來都一樣;敘事讓卡住的步驟自己報名 #208;observability/boot.py docstring
用單一 litellm CustomLogger 做忠實可重播的 LLM 呼叫紀錄 一個 callback 覆蓋每個呼叫點(runner/KB/retrieval/VLM/wiki/embeddings),無需 per-site 接線;record 可貼上重播,讓 operator 重發並調 prompt;必須不卡、不錯 always-stream-LLM / LLM-features-need-live-checks 記憶;observability/logger.py docstring
LLM 角色走單一 preset cascade,≥2 endpoint 變 busy-aware FallbackLlm 部署有多個各自可能「太忙」的模型;優先序清單讓系統在 busy/hang 時換下一個;單一 cascade 避免平行的扁平 config block 走樣 #196/#131;docs/plan-llm-failover.md;factories.LlmEndpoint/resolve_llm_chain
Job runner ⊥ API,以 run_consumers gate + 共用 build_coordinators 讓 API 當 pure producer 而專屬 worker pod 各自在自己的 HPA 下消費每個 JobType;coordinator 構造 FastAPI-free 讓 worker 沒有 HTTP app #312;coordinators.py docstring;ServerSettings.run_consumers
Postgres DSN 在 DSN 接縫注入 connect_timeout specstar 的 SQLAlchemy engine 沒有 timeout,DB 掛掉時首次連線(spec.apply 的 create_all)會卡數分鐘;DSN 是我們唯一擁有、會流到每個 store 的接縫 #208;factories._with_connect_timeout

與其他子系統的關係

  • 資料層(specstar)get_spec 從 filestore 設定組 BackendConfig 並呼叫 make_spec,把 default_user + superusers 穿進去讓 storage access_scope 與 API guard 一致。
  • API 與回合引擎create_app 是所有 factory 的匯流點 —— ~40 個 keyword seam 在此注入,run_consumers 的消費端與 route 接線也在這裡。
  • Sandbox、FileStore 與同步get_sandbox / get_filestore 是 Sandbox / FileStore Protocol 的唯一選擇點。
  • Agent 執行時get_runnerLitellmAgentRunner(含 failover chain);get_agent_config_catalogbuild_catalogSettings.agents 變成 runtime AgentConfigCatalog
  • App 平台get_app_catalog 在啟動時 validate_all_apps 驗證每個 App(#89)。
  • 知識庫:攝取與索引知識庫:檢索與 Agent:embedder、doc/chat pipeline、parser registry、retrieval/VLM/wiki/quality/card-drafter LLM 全在這裡建好並注入。
  • 背景工作與擴展coordinators.build_coordinators 與 worker entrypoint 共用 FastAPI-free 根,由 run_consumers gate(#312)。
  • 平台服務(健康 / 觀測 / 權限 / 使用者)get_check_registryget_replay_serviceget_sanity_llm_factory/get_sanity_models 由 settings 建好注入;install_llm_loggingCustomLogger append 到 litellm.callbacks。
  • 部署面(pod-split / HPA / worker 入口)見 部署

原始碼錨點

接手者建議優先讀的真實檔案:

  • src/workspace_app/__main__.py —— main():load_with_provenanceemit_config_dumpinstall_llm_loggingboot_step 鏈 → create_appuvicorn.run;get_user_id 接縫;run_consumers=settings.server.run_consumers
  • src/workspace_app/config/loader.py —— load_with_provenance_validate_TOP_SCHEMA_build_provenance_settings_from_dict
  • src/workspace_app/config/interpolate.py —— expand_envhas_env_referenceEnvVarUnset
  • src/workspace_app/config/schema.py —— SettingsServerSettings(default_user/superusers/run_consumers)。
  • src/workspace_app/config/catalog_build.py —— build_catalogresolve_usage_validate_kb_search_granted_KB_REQUIRED_PURPOSES
  • src/workspace_app/factories.py —— get_spec_backend_for_with_connect_timeoutget_sandboxget_filestoreget_runnerresolve_llm_chainLlmEndpoint_llm_from_chain/_vlm_from_chain
  • src/workspace_app/config/dump.py —— emit_config_dumprender_is_secret_mask
  • src/workspace_app/observability/boot.pysetup.pylogger.pyrecord.pywriter.py
  • src/workspace_app/api/app.py —— create_app 精簡組裝根(#54):組 deep module → register_*_routes 掛 route → build_lifespan → mount SPA;route handler 不再住這裡。
  • src/workspace_app/api/lifecycle.py —— build_lifespan:FastAPI lifespan、run_consumers 消費 gate(start_consuming ×4)、seed help collection boot_step(#230)、五個背景 sweeper。
  • src/workspace_app/coordinators.pysrc/workspace_app/worker/__main__.py