ทีมเอเยนต์ส่วนใหญ่ไม่ได้สร้างโครงสร้างพื้นฐานขึ้นมาเอง พวกเขาแค่เลือกใช้ตัวที่มีอยู่แล้ว LangChain, LangGraph, OpenAI Agents SDK, Anthropic SDK, CrewAI, AutoGen, ลูปเครื่องมือ, หน่วยความจำ, และการออเคสเตรชั่น — ทั้งหมดถูกหยิบมาจากชั้นวางเป็นแพ็กเกจเดียว โครงสร้างพื้นฐานคือเฟรมเวิร์กที่คุณ import เข้ามา ถ้ามีอะไรข้างในไม่เข้ากับคุณ คุณก็ fork มัน, สู้กับมัน, หรือหาทางอ้อม

ผมคิดว่าการออกแบบแบบนั้นมันผิด และนั่นคือสาเหตุที่ทีมเอเยนต์ที่ทำงานมานานทุกทีมสุดท้ายก็ต้องเขียนโครงสร้างพื้นฐานของตัวเองขึ้นมาใหม่ตั้งแต่ต้น เพราะโครงสร้างพื้นฐาน它不是สิ่งเดียว มันเป็นสิบหรือสิบสองสิ่งที่แตกต่างกันถูกมัดรวมกัน เพราะระบบนิเวศรอบข้างไม่มีวิธีให้คุณประกอบมันเข้าด้วยกัน Pi agent กำลังมาในทิศทางที่ถูกต้อง แต่ก็ยังอยู่ในกระบวนทัศน์ของ "เพิ่มบริการอีกตัว แล้วรวมเข้ากับตัวอื่นทั้งหมด" เอ็นจินของ iii ปฏิบัติต่อเวิร์กเกอร์ทุกตัวเหมือนกัน และกำจัดตรรกะการรวมเข้ากันทั้งหมด เราเตอร์ของผู้ให้บริการ, คลังเก็บข้อมูลประจำตัว, เอ็นจินนโยบาย, ประตูอนุมัติ, แค็ตตาล็อกโมเดล, พื้นที่จัดเก็บเซสชัน, ตัวติดตามงบประมาณ, การกระจายฮุคหลังการเรียก, และลูปเทิร์นที่ทนทาน — สิ่งเหล่านี้คือข้อกังวลที่เป็นอิสระต่อกัน ทั้งหมดนี้ทำงานร่วมกับคิว, เซิร์ฟเวอร์ HTTP/API, การสตรีม, หรือแม้แต่เวิร์กเกอร์ในเบราว์เซอร์ของคุณได้ เฟรมเวิร์กที่ส่งมาพร้อมกันเป็นก้อนเดียวกำลังขายการแลกเปลี่ยนที่คุณไม่จำเป็นต้องทำ
แนวคิดเบื้องหลัง iii คือสิ่งเหล่านี้ไม่ควรรวมเป็นก้อนเดียวกัน ควรมีชุดของเวิร์กเกอร์ที่ทำงานบนเอ็นจินร่วมกัน แต่ละตัวสามารถแทนที่ได้, แต่ละตัวมีเวอร์ชันของตัวเอง, แต่ละตัวเชื่อมต่อกันด้วยพรีมิทีฟเดียว: ทริกเกอร์ (iii.trigger()) ที่เวิร์กเกอร์ตัวอื่น ๆ ก็ใช้เช่นกัน โครงสร้างพื้นฐานกลายเป็นสแต็กของเวิร์กเกอร์ที่ติดตั้งได้ และ "สร้างเอง" ไม่ได้หมายถึง "fork เฟรมเวิร์ก" อีกต่อไป หมายถึง "สลับเวิร์กเกอร์สองสามตัว"
โพสต์นี้จะพาคุณไปดูว่าสิ่งนั้นมีหน้าตาเป็นอย่างไรจริง ๆ สแต็กที่สมบูรณ์ซึ่งขับเคลื่อนการทำงานของเอเยนต์ iii ในปัจจุบัน แต่ละเลเยอร์ทำไมถึงเป็นเวิร์กเกอร์ของตัวเอง และคุณจะแทนที่เลเยอร์ใด ๆ ได้อย่างไร
15 หน้าที่ที่ระบบฮาร์เนสของเอเยนต์ต้องทำ
ถ้าคุณแยกส่วนระบบฮาร์เนสของเอเยนต์ในระดับโปรดักชันกลับไปเป็นความรับผิดชอบของมัน คุณจะได้รายการประมาณนี้:
- รับคำขอเทิร์นจากไคลเอนต์และบันทึกไว้
- แก้ไขข้อมูลประจำตัวสำหรับผู้ให้บริการโมเดลที่จะถูกเรียก
- ค้นหาว่าโมเดลที่เลือกทำอะไรได้บ้าง (วิทัศน์, เครื่องมือ, การสตรีม, หน้าต่างบริบท)
- ขับเคลื่อนสถานะเครื่องจักรต่อเทิร์น, จัดเตรียม, สตรีมผู้ช่วย, เรียกใช้เครื่องมือ, ควบคุม, และทำลาย
- โหลดและให้บริการสกิลบอดี้ที่อธิบายรูปร่างของคำขอของแต่ละฟังก์ชัน, รหัสข้อผิดพลาด, และหมายเหตุการใช้งาน
- ประกอบ system prompt, ย่อหน้าโหมด, คำนำหน้าเอกลักษณ์, ไดเรกทอรีการทำงาน, และภาคผนวกสกิลเริ่มต้น
- สตรีมโทเค็นกลับไปยังไคลเอนต์เมื่อโมเดลสร้างมันขึ้นมา
- ตรวจสอบทุกการเรียกใช้เครื่อง tool call (ซึ่งก็คือฟังก์ชันหนึ่ง) กับนโยบายก่อนที่จะรัน
- หยุดการเรียกใช้ tool call ที่ต้องการการตัดสินใจของมนุษย์ชั่วคราว และส่งคำตอบกลับไปยังเทิร์นที่ถูกต้อง
- ติดตามค่าใช้จ่ายของ LLM ตามงบประมาณต่อเวิร์กสเปซหรือต่อเอเยนต์
- รันฮุคก่อนและหลังการเรียกใช้ tool call (การบันทึก, การแก้ไข, เอฟเฟกต์ข้างเคียงแบบกำหนดเอง)
- บันทึกเซสชันเป็นแผนผังแบบแตกกิ่ง เพื่อให้การ fork และ resume ทำงานได้
- บีบอัดประวัติเซสชันเมื่อหน้าต่างบริบทเต็ม
- ปล่อยสตรีมเหตุการณ์ที่ UI สมัครสมาชิก
- ส่วนที่ขาดหายไปจากการสร้างบริษัทเอเยนต์ทุกแห่งที่ผมเห็น: ต้องมี OpenTelemetry trace หนึ่งตัวครอบคลุมทุกขั้นตอน เพื่อให้คุณดีบักมันได้
ระบบฮาร์เนสของเอเยนต์ที่จริงจังทุกตัวทำหน้าที่เหล่านี้ส่วนใหญ่ ตัวที่แพงทำทั้งหมด ตัวถูกตัดมุมแล้วค่อยสร้างมุมเหล่านั้นใหม่เมื่อเจอโปรดักชัน เฟรมเวิร์กรวมพวกมันเป็นเสาหินและจัดส่งแต่ละตัวเพียงเวอร์ชันเดียว ส่วนสุดท้ายนี่แหละที่ทำให้คุณเสียค่าใช้จ่าย เพราะอีกหนึ่งปีให้หลัง คุณจะพบว่าเอ็นจินนโยบายที่คุณต้องการไม่ใช่เอ็นจินนโยบายที่เฟรมเวิร์กส่งมา และการแทนที่มันหมายถึงการแทนที่ระบบฮาร์เนสทั้งหมด
ระบบฮาร์เนสของ iii ส่งงานทั้งสิบสามอย่างนั้นเป็นเวิร์กเกอร์แยกกันบนทะเบียน workers.iii.dev แต่ละตัวใช้โปรโตคอล WebSocket เดียวกัน แต่ละตัวลงทะเบียนฟังก์ชันและทริกเกอร์บนบัสเอ็นจินเดียวกัน แต่ละตัวสามารถ iii worker add, สลับได้, และเขียนด้วยภาษาใดก็ได้ที่มี SDK
สแต็ก แยกตามเวิร์กเกอร์
นี่คือสแต็กโปรดักชันจริงจาก monorepo iii-hq/workers โดยแต่ละเวิร์กเกอร์มีหน้าที่ในหนึ่งบรรทัด ทั้งหมดอยู่ในแพ็กเกจที่ github.com/iii-hq/workers/harness:

11 เวิร์กเกอร์ หนึ่งเอ็นจิน แต่ละตัวมีเวอร์ชันที่เผยแพร่แล้ว แต่ละตัวสามารถรันเป็นกระบวนการแยกกัน (pnpm dev:<worker> ใน dev, iii worker add <specific-worker> เป็นไบนารีที่เผยแพร่) หรือเป็นส่วนหนึ่งของจุดเริ่มต้นแบบผสมที่รันพวกมันพร้อมกัน
เหตุผลที่สิ่งนี้สำคัญ: ทุกกล่องในตารางนั้นคือจุดที่ใครบางคนสามารถส่งเวิร์กเกอร์ที่แตกต่างให้คุณได้ และคุณยังคงใช้ส่วนที่เหลือไว้ ไม่ชอบแค็ตตาล็อกโมเดลแบบสแตติก? เสียบเวิร์กเกอร์ที่ลงทะเบียน models::list และอ่านจาก API สด ไม่ชอบการเก็บข้อมูลประจำตัวแบบไฟล์? เสียบเวิร์กเกอร์ที่ลงทะเบียน auth::get_token และอ่านจากตัวจัดการความลับ ต้องการ FSM ของเทิร์นอื่นสำหรับเวิร์กโฟลว์ที่แตกกิ่งต่างกัน? แทนที่ turn-orchestrator ทุกตัวที่พึ่งพามันเรียก run::start และอ่าน turn_state ผ่านบัสเดียวกัน ดังนั้นสแต็กที่เหลือไม่เปลี่ยนแปลง
วิธิการทำงานของลูปจริง ๆ
รูปร่างของหนึ่งเทิร์นเป็นแบบนี้ โดยไล่ตามลำดับการทำงานของเวิร์กเกอร์
เบราว์เซอร์/CLI/แชท POST เทิร์นผ่าน harness::trigger ด้วย {session_id, message_id, payload} เวิร์กเกอร์ harness meta-worker ส่งต่อ payload ไปยัง run::start การ hop นี้มีไว้เพื่อให้ตัวห่อ OpenTelemetry span สามารถกำหนด seed ให้กับ session ID และ message ID เป็น baggage ซึ่งจะแพร่กระจายไปยังทุกการเรียก iii.trigger ที่ซ้อนกันในทุกเวิร์กเกอร์ในสแต็ก ต้นไม้ trace อีกด้านหนึ่งเป็นกราฟที่เชื่อมต่อกันเป็นหนึ่งเดียว
run::start ไปถึง turn-orchestrator มันคงคำขอรัน, กำหนดค่าเริ่มต้นของ TurnStateRecord ใน iii state ที่ session/<sid>/turn_state และส่งคืนทันที งานจริงเกิดขึ้นภายในเครื่องสถานะที่ทนทานต่อเทิร์น ซึ่งถูกปลุกโดยการ publish ไปยัง FIFO ของ turn-step
สถานะปลายทางสองสถานะคือ stopped (ออกอย่างสะอาดผ่าน finishSession()) และ failed (การโยนข้อผิดพลาดจาก handler ที่ไม่คาดคิดจะมาจบที่นี่, ack คิวเพื่อให้มันหยุดลองใหม่, และแสดง message_complete{stop_reason:'error'} พร้อม agent_end เพื่อให้ UI แสดงเหตุผล) การ teardown เป็น finishSession() inline ที่เรียกจากเส้นทางจบเทิร์นใด ๆ ไม่ใช่ขั้นตอนที่ถูก enqueue แยกต่างหาก
provisioning ทำสามสิ่ง มันบูต microVM iii-sandbox ถ้าการรันต้องการการทำงานแบบแยกเดี่ยว มันเรียก directory::skills::download สำหรับทุก namespace ใน system_default_skills (ค่าเริ่มต้น ["iii://iii-directory/index"]) เพื่อให้ iii-directory แคชสกิลบอดี้ที่การรันเริ่มต้นด้วยไว้ล่วงหน้า และมันประกอบ system prompt เป็นสามชั้น: ย่อหน้าโหมดที่เลือกจาก run_request.mode (plan, ask, หรือ agent), คำนำหน้าเอกลักษณ์ของ iii ที่สอนโมเดลเกี่ยวกับแบบแผน agent_trigger และรูปแบบการค้นหาแบบ on-demand ของ directory::skills::get, และดัชนีที่ผนวกของสกิลเริ่มต้นที่เอเยนต์บูตด้วย ผู้เรียกสามารถแทนที่พรอมพ์ทั้งหมดได้โดยส่ง system_prompt บน run::start; มิฉะนั้น orchestrator จะสร้างมันขึ้นมา สคีมาฟังก์ชันมาจากแค็ตตาล็อกเอ็นจินที่ใช้งานจริง
assistant_streaming เรียก provider::<name>::stream บนเวิร์กเกอร์ผู้ให้บริการที่ตรงกับฟิลด์ provider ของรัน เวิร์กเกอร์ผู้ให้บริการดึงข้อมูลประจำตัวผ่าน auth::get_token (auth-credentials), สตรีมการตอบสนอง SSE ของโมเดลไปยังช่อง iii, และ orchestrator ระบายช่องนั้นโดยส่งเหตุการณ์ message_update บน agent::events สำหรับการกระจายไปยัง UI การสร้างช่องและลูปการอ่านอยู่เบื้องหลัง MessagePump แบบ pull-based ใน provider-stream.ts ดังนั้นสถานะการสตรีมยังคงจดจ่ออยู่กับการเปลี่ยนสถานะ
เมื่อผู้ช่วยส่งคืน tool call, FSM จะเข้าสู่ function_execute ทุก tool call ผ่าน dispatchWithHook ซึ่งเป็นจุดคอขวดเดียวใน orchestrator consultBefore เรียก policy::check_permissions โดยตรงโดยมี timeout 5 วินาที เวิร์กเกอร์นโยบาย (harness meta-worker ในสแต็กเริ่มต้น) อ่าน iii-permissions.yaml, จับคู่ function_id ของการเรียกกับชุดกฎ และส่งคืนหนึ่งในสามผลลัพธ์:
- allow: dispatch ดำเนินต่อไป; orchestrator ทริกเกอร์ฟังก์ชันเป้าหมายและเขียนผลลัพธ์
- deny: dispatch ถูกตัดจบด้วย DenialEnvelope ผลลัพธ์กลายเป็นบันทึกการปฏิเสธ
- needs_approval: การเรียกนั้นถูกจอดไว้ในลิสต์ awaiting_approval ของเทิร์น ส่วนที่เหลือของ batch ยังคง dispatch ต่อไป เทิร์นจะเปลี่ยนเป็น function_awaiting_approval เมื่อมีรายการที่รออยู่หนึ่งรายการขึ้นไปเท่านั้น
การปลุกการอนุมัติเป็นแบบ reactive และใช้ร่วมกัน orchestrator ลงทะเบียนทริกเกอร์สถานะ turn::on_approval เพียงตัวเดียวบนขอบเขต approvals เมื่อคอนโซลเรียก approval::resolve, approval-gate worker เขียน approvals/<sid>/<cid> = {decision, reason} ไปยัง iii state การเขียนนั้นจะยิง turn::on_approval ซึ่งทำให้เซสชันที่เกี่ยวข้องคืบหน้า function_awaiting_approval อ่านเฉพาะการตัดสินใจที่เพิ่งมาถึง, dispatch แต่ละอันเมื่อมันมาถึง (allow กลายเป็นการ dispatch ที่ได้รับการอนุมัติล่วงหน้า, deny หรือ aborted กลายเป็นการปฏิเสธสังเคราะห์), และคืบหน้าเมื่อ awaiting_approval[] ว่างเปล่า ไม่มีฟังก์ชัน resume ต่อการเรียกให้ลงทะเบียน ไม่ต้องสแกนใหม่เมื่อเริ่มต้นเพื่อกู้คืนการอนุมัติที่ค้างอยู่ ทริกเกอร์ตัวเดียวครอบคลุมทุกเซสชัน
Fail-closed โดยโครงสร้าง: ถ้าเวิร์กเกอร์นโยบายไม่สามารถเข้าถึงได้หรือ timeout 5 วินาทีเกิดขึ้น consultBefore จะปฏิเสธการเรียกด้วยซองจดหมาย gate_unavailable ถ้า iii::durable::publish เองเกิดข้อผิดพลาด, การกระจายฮุคจะส่งคืน publish_failed: true และ orchestrator จะถือว่ามันเป็นการปฏิเสธ
มีข้อดีด้านเวลาแฝงหลายอย่างที่เกิดขึ้นจากรูปร่างนี้ ฮุคหลังการเรียกฟังก์ชันจะลัดวงจร publish_collect ผ่านแคชการมีอยู่ของผู้สมัครสมาชิกเมื่อไม่มี subscriber แบบ durable ลงทะเบียนสำหรับทอปิกนั้น ซึ่งช่วยลดประมาณ 500ms ต่อการเรียกใช้ฟังก์ชันที่ดำเนินการ tearing_down ถูก inline ไว้ใน finishSession() ซึ่งลดการ hop ของ durable queue หนึ่งครั้งต่อเทิร์น context-compaction สมัครสมาชิกสตรีม agent::turn_end โดยเฉพาะที่ orchestrator ปล่อยที่ขอบเขตของเทิร์น ดังนั้นการปลุก compactor จึงเป็นต่อเทิร์นแทนที่จะเป็นต่อเหตุการณ์ ทริกเกอร์สถานะ session-create fanout เกตตามขอบเขตเท่านั้นและจับคู่ในกระบวนการ ดังนั้น RPC harness::session::is_create_event ก่อนหน้านี้ต่อการเขียนจึงหายไป
หลังจาก batch เสร็จสมบูรณ์ steering_check ตัดสินใจว่าจะดำเนินการต่อ, หยุด, หรือถึง max_turns ถ้าดำเนินการต่อ, วนกลับไปที่ assistant_streaming ถ้าหยุดหรือ max_turns, finishSession() ทำงานแบบ inline: ปล่อย agent_end, ปลดปล่อย sandbox, เปลี่ยนเป็น stopped
ตลอดการรันทั้งหมด, ทุกเวิร์กเกอร์ที่เข้าร่วมจะปล่อย OTel spans ที่ติดแท็กด้วย iii.session.id, iii.message.id, และ iii.function.id แท็กเหล่านั้นคือสิ่งที่ engine::traces::group_by ของเอ็นจินอ่านเพื่อเติม "Group by Session" / "Group by Message" / "Group by Function" ใน UI traces การ instrumentation เป็นแบบอัตโนมัติ: src/runtime/worker.ts ห่อทุก registerFunction ด้วย Proxy ดังนั้นไม่มีโค้ดต่อเวิร์กเกอร์ใดต้องจำเพิ่ม spans
สร้างเอง
ส่วนที่น่าสนใจคือไม่มีเวิร์กเกอร์ใดข้างต้นที่พิเศษ แต่ละตัวเป็นโพรเซสที่เปิด WebSocket ไปยังเอ็นจิน ลงทะเบียนฟังก์ชันและทริกเกอร์บางอย่าง แล้วรัน สัญญาเหมือนกับสัญญาที่เวิร์กเกอร์แอปพลิเคชันทุกตัวใช้ ระบบฮาร์เนสสร้างขึ้นบนพรีมิทีฟเดียวกันกับตรรกะทางธุรกิจของคุณ
ซึ่งหมายความว่า "สร้างระบบฮาร์เนสของคุณเอง" แยกย่อยเป็นการดำเนินการเดียวกับ "เขียนเวิร์กเกอร์ใด ๆ" คุณเลือกเลเยอร์ที่ต้องการแทนที่, คุณเขียนเวิร์กเกอร์ที่ลงทะเบียนฟังก์ชันเดียวกันบนบัส, คุณ iii worker add มัน, และสแต็กที่เหลือเริ่มใช้เวิร์กเกอร์ของคุณ
สองเลเยอร์ที่ไม่ปรากฏในตารางเวิร์กเกอร์ด้านบน แต่มีความสำคัญต่อพฤติกรรมของระบบฮาร์เนส สกิลคือวิธีที่เวิร์กเกอร์แต่ละตัวโฆษณาว่าฟังก์ชันของมันทำอะไร เวิร์กเกอร์ทุกตัวสามารถเผยแพร่สกิลที่ iii://<worker>/<function> ซึ่งเอเยนต์จะเรียกผ่าน directory::skills::get ก่อนเรียกฟังก์ชันนั้นเป็นครั้งแรก system prompt จะถูกประกอบต่อเทิร์นจากย่อหน้าโหมด, คำนำหน้าเอกลักษณ์ของ iii, และสกิลบอดี้เริ่มต้นที่การรันถูกกำหนดค่าให้ใช้ ทั้งสองอย่างขับเคลื่อนด้วยบัส: สกิลให้บริการโดยเวิร์กเกอร์ iii-directory, system prompt ประกอบโดย turn-orchestrator ทั้งสองอย่างสามารถแทนที่ได้
ห้าตัวอย่างที่เป็นรูปธรรม
แทนที่แค็ตตาล็อกโมเดลด้วย API สด เขียนเวิร์กเกอร์ที่ลงทะเบียน models::list, models::get, models::supports ให้มันดึงข้อมูลจากเอนด์พอยต์แค็ตตาล็อกของผู้ให้บริการของคุณทุก ๆ N นาทีและแคช เผยแพร่มัน iii worker add your-org/dynamic-models-catalog หยุดเวิร์กเกอร์ models-catalog แบบสแตติก turn-orchestrator ไม่รู้ว่ามีความแตกต่าง มันเรียก iii.trigger('models::list') และเอ็นจินจะเส้นทางไปยังเวิร์กเกอร์ที่ลงทะเบียนฟังก์ชันไอดีนั้นล่าสุด
เพิ่มผู้ให้บริการใหม่ รูปร่างของ provider-kimi และ provider-lmstudio พิสูจน์ให้เห็นแล้ว แต่ละตัวคือเวิร์กเกอร์ตัวเดียวที่ลงทะเบียน provider::<name>::stream และ provider::<name>::complete, ระบายสตรีม SSE จาก API ต้นทางไปยังช่อง iii, และเขียนการใช้งานโมเดลไปยัง llm-budget ผ่าน budget::record การเพิ่มผู้ให้บริการที่ห้าคือการเขียนโฟลเดอร์เดียวที่มี iii.worker.yaml หนึ่งอันและ register.ts หนึ่งอัน เผยแพร่ไปยัง registry หรือเก็บไว้ในเครื่อง turn-orchestrator เลือกผู้ให้บริการตามฟิลด์ provider ของรัน; ผู้ให้บริการใหม่จะพร้อมใช้งานทันทีที่เวิร์กเกอร์เชื่อมต่อ
ให้บริการสกิลจากที่เก็บอาร์ติแฟกต์ส่วนตัว เขียนเวิร์กเกอร์ที่ลงทะเบียน directory::skills::get และ directory::skills::list ซึ่ง backed โดยระบบเอกสารภายในของคุณหรือ S3 bucket ส่วนตัว ตัดการเชื่อมต่อหรือเปลี่ยนชื่อเวิร์กเกอร์ iii-directory เริ่มต้น การบูตสแตรปของ orchestrator เรียก directory::skills::download ต่อ namespace; เวิร์กเกอร์ของคุณตอบกลับ รูปแบบ "เรียกสกิลต่อฟังก์ชันก่อนเรียกฟังก์ชันใหม่" ของเอเยนต์ยังคงทำงานไม่เปลี่ยนแปลงเพราะรูปร่างของ wire เดิม
แทนที่ system prompt ทั้งหมด run::start รองรับฟิลด์ system_prompt ที่เป็น optional ส่งมันแล้ว orchestrator จะใช้สตริงของคุณตามตัวอักษร, ข้ามการประกอบย่อหน้าโหมด + คำนำหน้าเอกลักษณ์ + ภาคผนวกสกิล มีประโยชน์เมื่อคุณมีทรัพย์สินพรอมพ์ที่มีอยู่แล้วที่คุณต้องการให้ระบบฮาร์เนสให้เกียรติโดยไม่ต้องแก้ไข การดาวน์โหลดสกิลยังคงทำงานในการบูตสแตรป ดังนั้นเอเยนต์ยังคงมีความสามารถในการค้นหา directory::skills::get แบบ on-demand แม้จะมีพรอมพ์แบบกำหนดเอง
แทนที่พื้นผิว UI ของประตูอนุมัติ เวิร์กเกอร์ approval-gate เริ่มต้นลงทะเบียน approval::resolve สคีมา wire คือการเรียกฟังก์ชันเดียว:
handler จะคง approvals/<sid>/<cid> = {decision, reason} ไว้ใน iii state ทริกเกอร์สถานะ turn::on_approval เพียงตัวเดียวของ orchestrator จะรับการเขียนนั้นและปลุกเซสชันที่ถูกต้อง ถ้าคุณต้องการขับเคลื่อนการอนุมัติจาก Slack แทนคอนโซล, เขียนเวิร์กเกอร์ Slack ที่ฟังคำสั่ง slash command /approve <id> และ /deny <id>, แล้วเรียก approval::resolve ด้วย payload ที่ถูกต้อง orchestrator ไม่รู้ว่ามีความแตกต่าง เวิร์กเกอร์ approval-gate ทั้งหมดยังคงไม่ถูกแตะต้อง คุณเพิ่มเวิร์กเกอร์ใหม่; คุณไม่ได้แทนที่เวิร์กเกอร์ที่มีอยู่
ถ้าคุณต้องการเอ็นจินนโยบายอื่น (OPA, Cedar, DSL ของคุณเอง), เขียนเวิร์กเกอร์ที่ลงทะเบียน policy::check_permissions และส่งคืน { decision, rule_id?, matched_constraint? } ตัดการเชื่อมต่อเวิร์กเกอร์นโยบายเริ่มต้น (ซึ่งถูกห่อไว้ใน harness meta-worker, ดังนั้นคุณจะปิดการใช้งาน handler นั้นหรือรัน meta-worker ที่ลดขนาดลง) consultBefore ของ turn-orchestrator ไม่รู้ว่ามีความแตกต่าง timeout 5 วินาทีเดียวกัน, ความหมาย fail-closed เดียวกัน, รูปร่าง wire เดียวกัน
ประเด็นของตัวอย่างเหล่านี้ไม่ใช่การแทนที่เฉพาะเจาะจง แต่เป็นรูปร่างของการดำเนินการ ทุกเลเยอร์ของระบบฮาร์เนสในสแต็ก iii สามารถเข้าถึงได้ผ่านฟังก์ชันไอดีหนึ่งหรือสองตัวบนบัส การแทนที่เลเยอร์คือการเขียนเวิร์กเกอร์ที่ลงทะเบียนไอดีเหล่านั้น ส่วนที่เหลือของระบบยังคงอยู่
ระบบฮาร์เนสคือแถบเลื่อน ไม่ใช่ทางแยก
การถกเถียงเกี่ยวกับระบบฮาร์เนสแบบคลาสสิกมักตั้งกรอบเป็นแบบบางกับแบบหนา ลูปแบบบางของ Anthropic เทียบกับ DAG แบบชัดแจ้งของ LangGraph กรอบนี้สมมติว่าคุณเลือกข้างใดข้างหนึ่งแล้วอยู่กับมัน
เมื่อระบบฮาร์เนสประกอบด้วยเวิร์กเกอร์บนบัสเดียวกัน, แบบบางกับแบบหนาก็แค่จำนวนเวิร์กเกอร์ที่คุณติดตั้ง ระบบฮาร์เนสแบบบางคือ turn-orchestrator บวก provider-anthropic บวก auth-credentials บวก harness meta-worker ขนาดเล็กที่สุด เท่านั้นเอง ไม่มีการอนุมัติ, ไม่มีงบประมาณ, ไม่มีเอ็นจินนโยบาย, ไม่มีการกระจายฮุค รันอะไรก็ได้ ไว้ใจโมเดล มีประโยชน์สำหรับเอเยนต์วิจัยอิสระ, ลูปทดลอง, หรืออะไรก็ตามที่อยู่ภายใน
ระบบฮาร์เนสแบบหนาคือเวิร์กเกอร์ทั้งหมดสิบสามตัว บวก context-compaction บวกเวิร์กเกอร์นโยบายแบบกำหนดเอง บวก approval-gate แบบกำหนดเอง บวกพื้นผิวการอนุมัติที่รวมกับ Slack บวกเวิร์กเกอร์งบประมาณที่บังคับใช้ขีดจำกัดต่อเวิร์กสเปซ มีประโยชน์สำหรับเอเยนต์ที่ทำงานเวิร์กโฟลว์ของลูกค้าที่ทุก tool call ต้องตรวจสอบได้ และค่าใช้จ่ายโมเดลทุกบาทต้องรวมไปยังแดชบอร์ดการเงิน
ระยะห่างทางสถาปัตยกรรมระหว่างแบบบางและแบบหนาไม่ใช่การเขียนใหม่ มันคือการเปลี่ยน config โปรโตคอล wire เดียวกัน, รูปร่าง trace เดียวกัน, เรื่องการสังเกตการณ์เดียวกัน แถบเลื่อนเลื่อนโดยการเพิ่มและลบเวิร์กเกอร์ออกจาก config.yaml ของคุณ ทุกอย่างอื่นคงที่
มันใช้ได้ภายในเวิร์กเกอร์ตัวเดียวด้วย turn-orchestrator เพิ่งปล่อยรีแฟกเตอร์ที่ยุบ FSM จากสิบเอ็ดสถานะเหลือเจ็ด, ลบกลไก turn::approval_resume::<sid>/<cid> ต่อการเรียกเพื่อใช้ทริกเกอร์สถานะ turn::on_approval แบบ reactive เพียงตัวเดียวบนขอบเขต approvals, และ inline tearing_down เป็น finishSession() port เวิร์กเกอร์อื่นทุกตัวในสแต็ก (approval-gate, session, llm-budget, providers, models-catalog, auth-credentials, hook-fanout, context-compaction) ยังคงไม่เปลี่ยนแปลง รูปร่าง wire ของ approval::resolve ไม่ขยับ สัญญาคงอยู่ นั่นคือคุณสมบัติที่องค์ประกอบนี้มอบให้: การเขียนภายในครั้งใหญ่ของเวิร์กเกอร์ตัวเดียวคือการเปลี่ยนแปลงที่อยู่ในตัวเอง เพราะเพื่อนบ้านทุกตัวสื่อสารกับมันผ่านฟังก์ชันไอดีระดับบัส
นี่คือส่วนที่โมเดลเฟรมเวิร์กไม่สามารถให้คุณได้ เฟรมเวิร์กเลือกตำแหน่งบนแถบเลื่อนให้คุณแล้วล็อคคุณไว้ โมเดลเวิร์กเกอร์ปล่อยแถบเลื่อนไว้ในมือของคุณ
สิ่งนี้หมายถึงอะไรในทางปฏิบัติ
ถ้าคุณรันเอเยนต์บนเฟรมเวิร์กและรู้สึกถึงปัญหาเรื่องขอบเขตเดียวกันที่ทีมส่วนใหญ่เจอเมื่อขยายขนาด คำตอบน่าจะไม่ใช่ "เขียนระบบฮาร์เนสใหม่ในเฟรมเวิร์กของเราเอง" เอ็นจินนโยบายขยายไม่ได้ตามที่คุณต้องการ UI อนุมัติถูกเชื่อมเข้ากับพื้นผิวแชทของเฟรมเวิร์ก ที่เก็บข้อมูลประจำตัวไม่สามารถคุยกับตัวจัดการความลับของคุณได้ ตัวติดตามงบประมาณอยู่ในฐานข้อมูล sidecar ที่ trace มองไม่เห็น คำตอบคือเปลี่ยนไปใช้ซับสเตรตที่ระบบฮาร์เนสถูกแยกส่วนตั้งแต่แรก
วิธีที่เร็วที่สุดที่จะรู้สึกถึงข้อโต้แย้งนี้คือ clone github.com/iii-hq/workers, pnpm install, pnpm build, และรันจุดเริ่มต้นแบบผสม คุณจะได้ระบบฮาร์เนสสิบสี่เวิร์กเกอร์เต็มรูปแบบที่ชี้ไปยัง iii engine คุณสามารถปิดการใช้งานเวิร์กเกอร์ใดก็ได้โดยลบรายการออกจากลิสต์บูต คุณสามารถสลับเวิร์กเกอร์ใดก็ได้โดยเขียนตัวแทนที่ลงทะเบียนฟังก์ชันไอดีเดียวกัน คุณสามารถขยายเวิร์กเกอร์ใดก็ได้โดยเพิ่ม subscriber ไปยังทอปิกฮุคของมัน hook-fanout::publish_collect เป็น generic ที่ฮุค iii ทุกตัวสร้างขึ้น
เอกสารอยู่ที่ iii.dev/docs เอ็นจินอยู่ที่ github.com/iii-hq/iii ทะเบียนเวิร์กเกอร์อยู่ที่ workers.iii.dev ชุดรวมระบบฮาร์เนสอยู่ที่ github.com/iii-hq/workers/harness
แนวคิด
ระบบฮาร์เนสไม่ใช่สิ่งที่คุณติดตั้ง ระบบฮาร์เนสคือชุดของงานที่ระบบของคุณต้องทำเพื่อให้เอเยนต์ทำงานได้อย่างทนทาน, ปลอดภัย, และสังเกตได้ ยุคของเฟรมเวิร์กมัดรวมงานเหล่านั้นเข้าด้วยกันเพราะไม่มีอะไรด้านล่างให้วิธีประกอบมัน
แนวคิดของ iii คือพรีมิทีฟเดียว: เวิร์กเกอร์ที่เชื่อมต่อกับเอ็นจินผ่าน WebSocket และลงทะเบียนฟังก์ชันและทริกเกอร์ มีขนาดเล็กพอที่จะรองรับทุกงานเหล่านั้นแยกกัน, และสแต็กที่ได้จะมีประโยชน์มากกว่าเฟรมเวิร์กใด ๆ เพราะทุกเลเยอร์สามารถแทนที่ได้อย่างอิสระ
คุณไม่ใช่ "adopt" ระบบฮาร์เนสของ iii คุณติดตั้งเวิร์กเกอร์ที่คุณต้องการ, เขียนเวิร์กเกอร์ที่คุณต้องการ, และจบลงด้วยระบบฮาร์เนสที่มีรูปร่างตรงกับระบบของคุณทุกประการ โปรโตคอลเดียวกันในทุกเลเยอร์ trace เดียวกันในทุกการเรียก iii worker add เดียวกันสำหรับส่วนที่คุณนำมาจาก registry และสำหรับส่วนที่คุณเผยแพร่เอง
นี่คือหน้าตาของ "สร้างระบบฮาร์เนสของเอเยนต์ของคุณเอง" เมื่อซับสเตรตมีรูปร่างที่ถูกต้อง เลือกเวิร์กเกอร์ เขียนเวิร์กเกอร์ที่ขาดหายไป ประกอบ ระบบฮาร์เนสคือการประกอบ
ร่วมสร้างระบบฮาร์เนสของเอเยนต์ที่สมบูรณ์แบบที่โลกยุคใหม่ต้องการ: discord.gg/iiidev
iii เป็นโอเพนซอร์ส เริ่มต้นได้ที่ iii.dev/docs เวิร์กเกอร์ระบบฮาร์เนสอยู่ที่ github.com/iii-hq/workers และเอ็นจินอยู่ที่ github.com/iii-hq/iii
— Mike Piccolo, ผู้ก่อตั้งและ CEO @iiidevs





