Agents¶
เอเจนต์คือชั้น logic เชิงตัวแทน (agentic) ของ OpenJarvis มันกำหนดว่าคำถาม
จะถูกประมวลผลอย่างไร -- ไม่ว่าจะไปที่โมเดลโดยตรง, ผ่านลูปการเรียกใช้ tool,
ผ่านการให้เหตุผลแบบ ReAct, การรันโค้ดแบบ CodeAct, การแตกย่อยแบบ recursive
หรือ runtime ของเอเจนต์ภายนอก เอเจนต์ทั้งหมด implement ABC BaseAgent และ
ลงทะเบียนผ่าน AgentRegistry
ภาพรวม¶
| เอเจนต์ | Registry Key | accepts_tools |
หลายรอบ | คำอธิบาย |
|---|---|---|---|---|
SimpleAgent |
simple |
ไม่ | ไม่ | คำถาม-คำตอบแบบรอบเดียว |
OrchestratorAgent |
orchestrator |
ใช่ | ใช่ | ลูปการเรียกใช้ tool แบบหลายรอบ (function_calling + structured) |
NativeReActAgent |
native_react |
ใช่ | ใช่ | ลูป Thought-Action-Observation |
NativeOpenHandsAgent |
native_openhands |
ใช่ | ใช่ | การรันโค้ดแบบ CodeAct + การเรียก tool |
RLMAgent |
rlm |
ใช่ | ใช่ | Recursive LM พร้อม REPL แบบต่อเนื่อง |
OpenHandsAgent |
openhands |
ไม่ | ใช่ | ห่อ openhands-sdk จริง |
ClaudeCodeAgent |
claude_code |
ไม่ | ใช่ | Claude Agent SDK ผ่าน Node.js subprocess |
OpenCodeAgent |
opencode |
ไม่ | ใช่ | เอเจนต์เขียนโค้ด opencode บน engine ในเครื่องของคุณ |
OperativeAgent |
operative |
ใช่ | ใช่ | เอเจนต์ตั้งเวลาแบบต่อเนื่อง พร้อมจัดการ state |
MonitorOperativeAgent |
monitor_operative |
ใช่ | ใช่ | เอเจนต์ระยะยาว พร้อม 4 แกนกลยุทธ์ที่ตั้งค่าได้ |
Persona แบบถาวร: SOUL.md, MEMORY.md, USER.md¶
System prompt ของเอเจนต์ทุกตัวถูกประกอบขึ้นตอนเริ่มบทสนทนาโดย
SystemPromptBuilder ซึ่งแทรกไฟล์ Markdown เสริมได้สูงสุดสามไฟล์ -- persona
แบบถาวร ไฟล์เหล่านี้เป็นข้อความล้วนที่คุณเป็นเจ้าของและแก้ไขได้ ถูกโหลด
ตอนเริ่มบทสนทนาแต่ละครั้ง ไม่มี vector database หรือ embedding cache
เบื้องหลัง
| ไฟล์ | สิ่งที่เก็บไว้ | ตัวอย่างบรรทัด |
|---|---|---|
SOUL.md |
เอเจนต์ควรมีพฤติกรรมอย่างไร -- โทน, ความยาว, สิ่งที่ควรทักท้วง | Be concise. Challenge weak assumptions. |
MEMORY.md |
ข้อเท็จจริงเกี่ยวกับคุณ, โปรเจกต์ของคุณ, ความชอบของคุณ | I deploy to Postgres, never MySQL. |
USER.md |
คุณคือใคร -- role, ทีม, บริบท | Backend engineer at Acme, on the payments team. |
Persona นี้แตกต่างจาก memory backend แบบ retrieval: persona คือบริบท Markdown ที่เปิดใช้งานตลอดเวลา ถูกโหลดเข้า prompt ในขณะที่ memory backend คือที่เก็บระยะยาวที่ค้นหาได้ ซึ่งเอเจนต์ query ตามความต้องการ
อยู่ที่ไหน¶
โดยค่าเริ่มต้น ไฟล์เหล่านี้ถูกอ่านจากไดเรกทอรี config:
(ไดเรกทอรี config เคารพ $OPENJARVIS_HOME / $XDG_DATA_HOME เมื่อตั้งค่าไว้)
Path เหล่านี้ตั้งค่าได้ใต้ [memory_files]:
[memory_files]
soul_path = "~/.openjarvis/SOUL.md"
memory_path = "~/.openjarvis/MEMORY.md"
user_path = "~/.openjarvis/USER.md"
persona_name = "" # optional named persona -- see below
โหลดอย่างไร¶
ตอนเริ่มบทสนทนาแต่ละครั้ง SystemPromptBuilder จะอ่านแต่ละไฟล์เป็น UTF-8
และเพิ่มเนื้อหาเป็นส่วนหนึ่งของ system prompt หลังจาก template ของเอเจนต์
และก่อนแคตตาล็อก skill:
- ทั้งสามไฟล์เป็นตัวเลือกเสริม ไฟล์ที่ไม่มีหรือว่างเปล่าจะถูกข้าม ดังนั้น subset ใดก็ใช้งานได้ และการติดตั้งที่ไม่มีไฟล์ persona เลยจะทำงานเหมือนเดิมทุกประการ
- การแก้ไขมีผลกับบทสนทนาถัดไป ไฟล์ถูกอ่านครั้งเดียวตอนสร้าง prompt ของบทสนทนา จึงไม่ต้อง restart หรือ re-index -- แก้หรือลบบรรทัดแล้วมีผลทันทีในบทสนทนาถัดไป
- แต่ละส่วนมีการจำกัดความยาว ไฟล์จะถูกตัดให้อยู่ในงบตัวอักษรต่อส่วน เพื่อไม่ให้
MEMORY.mdขนาดใหญ่แย่งพื้นที่ prompt ส่วนที่เหลือ
Persona แบบมีชื่อ¶
การติดตั้งเดียวตอบเป็น persona ที่ต่างกันได้โดยไม่ต้องเปลี่ยน config ทั้งระบบ Persona แบบมีชื่ออยู่ในไดเรกทอรีของตัวเอง:
~/.openjarvis/personas/<name>/SOUL.md
~/.openjarvis/personas/<name>/MEMORY.md
~/.openjarvis/personas/<name>/USER.md
เลือกได้ต่อการเรียกใช้ หรือปิดใช้งานทั้งหมดก็ได้:
jarvis ask --persona work "summarize my open PRs"
jarvis ask --persona none "what is 2 + 2?" # inject no persona
ตั้งค่า persona_name ใต้ [memory_files] เพื่อให้ persona แบบมีชื่อเป็นค่า
เริ่มต้น persona_name = "none" (เทียบเท่ากับ --persona none) ปิดการแทรก
persona สำหรับการรันนั้น
แก้ไขไฟล์เหล่านี้¶
SOUL.md, MEMORY.md และ USER.md เป็น Markdown ล้วน -- เปิดด้วย editor
ใดก็ได้ MEMORY.md และ USER.md ยังอัปเดตได้โดยเอเจนต์เองผ่าน tool
memory_manage และ user_profile_manage เมื่อเปิดใช้งาน tool เหล่านั้น
ทำให้เอเจนต์บันทึกข้อเท็จจริงใหม่ระหว่างบทสนทนาได้ tool เหล่านี้เล็งเป้าไป
ที่ MEMORY.md และ USER.md เริ่มต้นเสมอ (ใต้ ~/.openjarvis/) ไม่ใช่สำเนา
ของ persona แบบมีชื่อ -- แก้ไขไฟล์เหล่านั้นด้วยตนเอง
BaseAgent ABC¶
เอเจนต์ทั้งหมด extend คลาส abstract BaseAgent
from abc import ABC, abstractmethod
from openjarvis.agents._stubs import AgentContext, AgentResult
class BaseAgent(ABC):
agent_id: str
accepts_tools: bool = False
def __init__(
self,
engine: InferenceEngine,
model: str,
*,
bus: Optional[EventBus] = None,
temperature: float = 0.7,
max_tokens: int = 1024,
) -> None: ...
@abstractmethod
def run(
self,
input: str,
context: AgentContext | None = None,
**kwargs,
) -> AgentResult:
"""Execute the agent on the given input."""
Class attribute accepts_tools ควบคุมว่าเอเจนต์รับ tool ผ่าน --tools บน
CLI หรือ tools= ใน SDK ได้หรือไม่ เอเจนต์ที่มี accepts_tools = False
จะไม่สนใจ argument ของ tool
BaseAgent ยังมี helper method ที่ implement แล้ว (_emit_turn_start,
_emit_turn_end, _build_messages, _generate, _max_turns_result,
_strip_think_tags) ที่ subclass ใช้เพื่อไม่ต้องเขียน logic ที่ซ้ำกัน ดู
เอกสารสถาปัตยกรรม สำหรับรายละเอียด
ToolUsingAgent คือคลาส base ระดับกลาง (extend BaseAgent) ที่ตั้งค่า
accepts_tools = True และเพิ่ม ToolExecutor กับขีดจำกัดลูป max_turns
เอเจนต์ที่ใช้ tool ทั้งหมด extend คลาสนี้
AgentContext¶
Context ตอน runtime ที่ส่งให้เอเจนต์ในแต่ละการเรียก
| ฟิลด์ | ชนิด | คำอธิบาย |
|---|---|---|
conversation |
Conversation |
ประวัติข้อความ (เติมล่วงหน้าด้วยบริบทหากการแทรก memory ทำงานอยู่) |
tools |
list[str] |
ชื่อ tool ที่เอเจนต์ใช้ได้ |
memory_results |
list[Any] |
ผลลัพธ์การดึงข้อมูล memory ที่ดึงมาล่วงหน้า |
metadata |
dict[str, Any] |
Metadata ใดๆ สำหรับการรัน |
AgentResult¶
ผลลัพธ์ที่คืนมาหลังจากเอเจนต์รันเสร็จ
| ฟิลด์ | ชนิด | คำอธิบาย |
|---|---|---|
content |
str |
ข้อความคำตอบสุดท้าย |
tool_results |
list[ToolResult] |
ผลลัพธ์จากการรัน tool ระหว่างการรัน |
turns |
int |
จำนวนรอบ (การเรียก inference) ที่ใช้ |
metadata |
dict[str, Any] |
Metadata ใดๆ เกี่ยวกับการรัน |
SimpleAgent¶
SimpleAgent คือเอเจนต์แบบรอบเดียวที่ส่งคำถามตรงไปยัง inference engine และ
คืนคำตอบ ไม่รองรับการเรียกใช้ tool
ทำงานอย่างไร:
- สร้างรายการข้อความจากบริบทของบทสนทนา (ถ้ามี) รวมกับคำถามของผู้ใช้
- เรียก inference engine ผ่าน
_generate() - คืนคำตอบเป็น
AgentResultพร้อมturns=1
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
1024 |
จำนวน token สูงสุดที่จะสร้าง |
เมื่อไหร่ควรใช้: สำหรับคำถาม-คำตอบแบบตรงไปตรงมา ที่ไม่ต้องเรียก tool หรือ ให้เหตุผลแบบหลายรอบ
OrchestratorAgent¶
OrchestratorAgent คือเอเจนต์แบบหลายรอบที่ implement ลูปการเรียกใช้ tool
เป็นเอเจนต์หลักสำหรับคำถามที่ต้องการการคำนวณ, การดึงความรู้ หรือการให้เหตุผล
แบบมีโครงสร้าง Extend ToolUsingAgent
ทำงานอย่างไร:
- สร้างรายการข้อความเริ่มต้นจากบริบทและคำถามของผู้ใช้
- ส่งข้อความพร้อมนิยาม tool (รูปแบบ function-calling ของ OpenAI) ไปยัง engine
- หาก engine ตอบกลับด้วย
tool_callsToolExecutorจะ dispatch แต่ละการเรียก - ผลลัพธ์ tool จะถูกต่อไว้เป็นข้อความ
TOOLและลูปจะทำงานต่อ - หากไม่มี
tool_callsคืนมา คำตอบจะถือว่าเป็นคำตอบสุดท้าย - ลูปจะหยุดหลังจากผ่านไป
max_turnsรอบ (ค่าเริ่มต้น: 10) โดยคืนเนื้อหาที่มีอยู่พร้อม metadata flagmax_turns_exceeded
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool ที่จะเปิดให้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
10 |
จำนวนรอบการเรียกใช้ tool สูงสุด |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
1024 |
จำนวน token สูงสุดที่จะสร้าง |
mode |
str |
"function_calling" |
โหมดการเรียก tool (function_calling หรือ structured) |
system_prompt |
str |
None |
System prompt กำหนดเอง |
เมื่อไหร่ควรใช้: สำหรับคำถามที่ต้องการการคำนวณ, ค้นหา memory, เรียกโมเดล ย่อย, อ่านไฟล์ หรือการให้เหตุผลแบบหลายขั้นตอน
ลูปการเรียกใช้ Tool
orchestrator ทำตามข้อตกลง function-calling ของ OpenAI engine ต้องรองรับการคืน tool_calls ในผลลัพธ์ ลูปจึงจะทำงาน หากส่ง tool ไปแล้วแต่ engine ไม่คืน tool call ใดๆ เอเจนต์จะทำงานเหมือนเอเจนต์แบบรอบเดียว
NativeReActAgent¶
NativeReActAgent implement ลูป Thought-Action-Observation ตามรูปแบบ
ReAct มัน prompt LLM ให้สร้างผลลัพธ์แบบมีโครงสร้าง (Thought:, Action:,
Action Input:, Final Answer:) และ parse คำตอบเพื่อขับเคลื่อนการรัน tool
Extend ToolUsingAgent
ทำงานอย่างไร:
- สร้าง system prompt พร้อมคำอธิบาย tool แบบเสริมข้อมูล (ชื่อ, parameter schema, หมวดหมู่) ผ่าน
build_tool_descriptions()การ parse ไม่สนใจตัวพิมพ์เล็ก/ใหญ่ - สร้างคำตอบและ parse ผลลัพธ์ที่มีโครงสร้างแบบ ReAct
- หากพบ
Final Answer:คืนค่านั้น - หากพบ
Action:รัน tool และป้อนผลลัพธ์กลับเป็นObservation: - วนซ้ำจนกว่าจะได้คำตอบสุดท้าย หรือเกิน
max_turns
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool ที่จะเปิดให้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
10 |
จำนวนรอบการให้เหตุผลสูงสุด |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
1024 |
จำนวน token สูงสุดที่จะสร้าง |
เมื่อไหร่ควรใช้: สำหรับคำถามที่ได้ประโยชน์จากการให้เหตุผลทีละขั้นตอนแบบ ชัดเจนร่วมกับการใช้ tool ที่คุณต้องการเห็นกระบวนการคิดของเอเจนต์
ความเข้ากันได้ย้อนหลัง
Registry alias "react" ชี้ไปยัง NativeReActAgent การ import แบบเดิม from openjarvis.agents.react import ReActAgent ก็ยังใช้งานได้เช่นกัน
NativeOpenHandsAgent¶
NativeOpenHandsAgent คือเอเจนต์แบบ CodeAct ที่สร้างและรันโค้ด Python
ควบคู่กับการเรียก tool แบบมีโครงสร้าง มันดึงเนื้อหา URL จาก input ของผู้ใช้
ล่วงหน้าเพื่อให้บริบทตรงกับ LLM ได้ด้วย Extend ToolUsingAgent
ทำงานอย่างไร:
- สร้าง system prompt แบบละเอียดพร้อมคำอธิบาย tool แบบเสริมข้อมูล (ผ่าน builder
build_tool_descriptions()ที่ใช้ร่วมกัน) และคำสั่งการรันโค้ด - ดึงเนื้อหา URL ใดๆ ใน input ของผู้ใช้ล่วงหน้า แล้วแทรกเนื้อหาเข้าไปโดยตรง
- ในแต่ละรอบ สร้างคำตอบและพยายามดึง code block หรือ tool call ออกมา
- โค้ดถูกรันผ่าน
code_interpreter; tool call ถูก dispatch ผ่านToolExecutor - หากไม่พบทั้งคู่ คืนเนื้อหาเป็นคำตอบสุดท้าย
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool ที่จะเปิดให้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
3 |
จำนวนรอบสูงสุด |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
2048 |
จำนวน token สูงสุดที่จะสร้าง |
เมื่อไหร่ควรใช้: สำหรับคำถามที่เกี่ยวกับเนื้อหา URL, การรันโค้ด หรืองาน ที่ LLM เขียนและรัน Python เพื่อแก้ปัญหาได้
RLMAgent¶
RLMAgent implement การแตกย่อยแบบ recursive ผ่าน REPL แบบต่อเนื่อง อิงจาก
paper RLM บริบทถูกเก็บเป็นตัวแปร Python แทนที่จะถูกแทรกเข้า prompt ทำให้
ประมวลผล input ยาวเท่าใดก็ได้ผ่านการเรียก sub-LM แบบ recursive Extend
ToolUsingAgent
ทำงานอย่างไร:
- สร้าง REPL แบบต่อเนื่องพร้อม callback
llm_query()และllm_batch() - แทรกบริบทจาก
AgentContextเข้า REPL เป็นตัวแปร - สร้างโค้ดและรันใน REPL
- หากมีการเรียก
FINAL(value)คืนค่านั้นเป็นคำตอบสุดท้าย - หากไม่พบ code block ถือเนื้อหาเป็นคำตอบข้อความโดยตรง
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool (เสริม) |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
10 |
จำนวนรอบการรันโค้ดสูงสุด |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
2048 |
จำนวน token สูงสุดที่จะสร้าง |
sub_model |
str |
เหมือน model |
โมเดลสำหรับการเรียก sub-LM |
sub_temperature |
float |
0.3 |
Temperature สำหรับการเรียก sub-LM |
sub_max_tokens |
int |
1024 |
Token สูงสุดสำหรับการเรียก sub-LM |
max_output_chars |
int |
10000 |
จำนวนตัวอักษรผลลัพธ์ REPL สูงสุด |
system_prompt |
str |
RLM_SYSTEM_PROMPT |
Override system prompt |
เมื่อไหร่ควรใช้: สำหรับงานบริบทยาวที่ได้ประโยชน์จากการแตกย่อยแบบ recursive เช่น การสรุปเอกสารขนาดใหญ่, การประมวลผลข้อมูลมีโครงสร้าง หรืองาน ที่ต้องจัดการบริบทเชิงโปรแกรม
OpenHandsAgent (SDK)¶
OpenHandsAgent ห่อแพ็กเกจ openhands-sdk จริง สำหรับการพัฒนาซอฟต์แวร์ที่
ขับเคลื่อนด้วย AI Extend BaseAgent โดยตรง (การจัดการ tool ถูกจัดการโดย SDK
ภายใน)
ทำงานอย่างไร:
- Import
openhands.sdkตอน runtime - สร้าง LLM, Agent และ Conversation จาก SDK
- ส่ง input และรันบทสนทนา
- คืนเนื้อหาข้อความสุดท้าย
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine (fallback) |
model |
str |
-- | Identifier ของโมเดล |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
temperature |
float |
0.7 |
Temperature ของ sampling |
max_tokens |
int |
1024 |
จำนวน token สูงสุดที่จะสร้าง |
workspace |
str |
os.getcwd() |
Working directory ของเอเจนต์ |
api_key |
str |
$LLM_API_KEY |
API key สำหรับผู้ให้บริการ LLM |
เมื่อไหร่ควรใช้: สำหรับงานพัฒนาซอฟต์แวร์ (ดีบัก, แก้ไขโค้ด, แก้เทสต์) ที่ OpenHands SDK มี runtime เอเจนต์พัฒนาซอฟต์แวร์แบบเต็ม
Dependency เสริม
ต้องมี openhands-sdk (uv sync --extra openhands) และ Python 3.12+
การใช้เอเจนต์¶
ผ่าน CLI¶
# Simple agent
jarvis ask --agent simple "What is the capital of France?"
# Orchestrator with tools
jarvis ask --agent orchestrator --tools calculator,think "What is sqrt(256)?"
# NativeReActAgent
jarvis ask --agent native_react --tools calculator "What is 2+2?"
# ReAct alias (same as native_react)
jarvis ask --agent react --tools calculator,think "Solve step by step: 15% of 340"
# NativeOpenHandsAgent
jarvis ask --agent native_openhands --tools calculator,web_search "Summarize example.com"
# RLMAgent
jarvis ask --agent rlm "Summarize this long document"
# OpenHands SDK agent
jarvis ask --agent openhands "Fix the bug in test_utils.py"
ผ่าน Python SDK¶
from openjarvis import Jarvis
j = Jarvis()
# Simple agent
response = j.ask("Hello", agent="simple")
# Orchestrator with tools
response = j.ask(
"Calculate 15% of 340",
agent="orchestrator",
tools=["calculator"],
)
# NativeReActAgent with tools
response = j.ask(
"What is sqrt(256)?",
agent="native_react",
tools=["calculator", "think"],
)
# Full result with tool details
result = j.ask_full(
"What is the square root of 144?",
agent="orchestrator",
tools=["calculator", "think"],
)
print(result["content"])
print(result["turns"])
print(result["tool_results"])
j.close()
ClaudeCodeAgent¶
ClaudeCodeAgent ห่อ SDK @anthropic-ai/claude-code ผ่าน Node.js
subprocess bridge ที่มาพร้อม ต่างจากเอเจนต์อื่นตรงที่การอนุมานถูกจัดการทั้งหมด
โดย Claude Agent SDK -- พารามิเตอร์ engine ถูกรับไว้เพื่อให้สอดคล้องกับ
interface ของ BaseAgent เท่านั้น ไม่ได้ถูกใช้จริง
สิ่งที่ต้องมี
ต้องมี Node.js 22+ อยู่ใน PATH และ environment variable ANTHROPIC_API_KEY (หรือส่ง api_key= โดยตรง) ตัวรันที่มาพร้อมจะถูกติดตั้งอัตโนมัติที่ ~/.openjarvis/claude_code_runner/ ตอนใช้งานครั้งแรกผ่าน npm install
ทำงานอย่างไร:
- ในการเรียกครั้งแรก คัดลอก
claude_code_runner/ที่มาพร้อม ไปยัง~/.openjarvis/claude_code_runner/และรันnpm install --productionหากไม่มีnode_modules - สร้าง JSON request payload (prompt, API key, workspace, allowed tools, system prompt, session ID) แล้วส่งไปยัง
stdinของ subprocessnode dist/index.js - ตัวรัน Node.js เรียก Claude Agent SDK และเขียน JSON ที่คั่นด้วย sentinel ลง
stdout - ฝั่ง Python parse ผลลัพธ์ระหว่าง marker
---OPENJARVIS_OUTPUT_START---และ---OPENJARVIS_OUTPUT_END---ดึงเนื้อหา, ผลลัพธ์ tool และ metadata ออกมา - คืน
AgentResultพร้อมturns=1
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | รับไว้เพื่อความสอดคล้องของ interface; ไม่ได้ใช้ |
model |
str |
-- | รับไว้เพื่อความสอดคล้องของ interface; ไม่ได้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
temperature |
float |
0.7 |
รับไว้เพื่อความสอดคล้องของ interface; ไม่ได้ใช้ |
max_tokens |
int |
1024 |
รับไว้เพื่อความสอดคล้องของ interface; ไม่ได้ใช้ |
api_key |
str |
$ANTHROPIC_API_KEY |
API key ของ Anthropic |
workspace |
str |
os.getcwd() |
Working directory ของเอเจนต์ Claude |
session_id |
str |
"" |
Session ID เสริม สำหรับความต่อเนื่องของบทสนทนา |
allowed_tools |
list[str] |
None (ทั้งหมด) |
ชื่อ tool ของ Claude Code ที่อนุญาต |
system_prompt |
str |
"" |
System prompt เพิ่มเติมสำหรับเอเจนต์ |
timeout |
int |
300 |
Timeout ของ subprocess เป็นวินาที |
เมื่อไหร่ควรใช้: สำหรับงานวิศวกรรมซอฟต์แวร์ที่ tool ในตัวของ Claude Agent SDK (แก้ไขโค้ด, รัน bash, จัดการไฟล์) มีความสามารถเหนือกว่าเอเจนต์แบบ เรียก tool ของ OpenJarvis
from openjarvis.agents.claude_code import ClaudeCodeAgent
agent = ClaudeCodeAgent(
engine=None, # not used
model="", # not used
workspace="/path/to/project",
allowed_tools=["Read", "Write", "Bash"],
timeout=120,
)
result = agent.run("Add type hints to all functions in utils.py")
print(result.content)
accepts_tools = False
ClaudeCodeAgent ไม่รับ tool ของ OpenJarvis ผ่าน --tools การเข้าถึง tool ของเอเจนต์ Claude ตั้งค่าแยกต่างหากผ่านพารามิเตอร์ constructor allowed_tools ซึ่งส่งชื่อ tool ที่ Claude Agent SDK เข้าใจเอง
OpenCodeAgent¶
OpenCodeAgent มอบหมายงานเขียนโค้ดให้ opencode
เอเจนต์เขียนโค้ด open-source โดยรันบน engine ในเครื่องของคุณ opencode
จัดการลูปเชิงตัวแทน, การแก้ไขไฟล์ และการใช้ tool; OpenJarvis จัดหาโมเดล --
รักษางานเอเจนต์เขียนโค้ดให้เป็น local-first
สิ่งที่ต้องมี
ต้องมี binary opencode อยู่ใน PATH (npm i -g opencode-ai หรือ brew install anomalyco/tap/opencode) มัน**ไม่**ได้มาให้ในตัว; run() จะคืน error ที่ชัดเจนหากไม่พบ ไม่ต้องมี ANTHROPIC_API_KEY -- การอนุมานไปผ่าน engine ของ OpenJarvis ของคุณ
ทำงานอย่างไร:
- หา base URL ที่เข้ากันได้กับ OpenAI จาก
engine(เช่น Ollama/vLLM/llama.cpp ที่<host>/v1) และเขียนopencode.jsonใน workspace ลงทะเบียนเป็น provider@ai-sdk/openai-compatible(openjarvis/<model>) - เปิด
opencode serveแบบ headless (loopback, port สุ่ม) และรอ/global/health - สร้าง session (
POST /session) และส่งงาน (POST /session/{id}/message) พร้อมmodel={providerID, modelID}และagentที่เลือก (buildหรือplan) - Parse
partsของข้อความที่คืนมา — ส่วนข้อความ →content, ส่วน tool →tool_results— เป็นAgentResult close()จะปิด session/server
พารามิเตอร์ของ Constructor (คัดมา):
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | ใช้หา URL ของ provider ที่เข้ากันได้กับ OpenAI ในเครื่อง |
model |
str |
-- | id ของโมเดลที่ให้บริการที่ provider (เช่น qwen3:8b) |
workspace |
str |
os.getcwd() |
ไดเรกทอรีที่ opencode ทำงานอยู่ |
agent |
str |
"build" |
เอเจนต์ opencode: build (เข้าถึงเต็ม) หรือ plan (อ่านอย่างเดียว) |
provider_base_url |
str |
อิงจาก engine | Override URL ฐานของ OpenAI ที่อิงจาก engine |
provider_id |
str |
"openjarvis" |
id ของ provider opencode ที่จะลงทะเบียน/ใช้ |
model_id |
str |
model |
id ของโมเดลภายใน provider |
server_password |
str |
$OPENCODE_SERVER_PASSWORD |
basic-auth เสริมสำหรับเซิร์ฟเวอร์ opencode |
timeout |
int |
600 |
Timeout ของ HTTP เป็นวินาที |
from openjarvis.agents.opencode import OpenCodeAgent
agent = OpenCodeAgent(engine, "qwen3:8b", workspace="/path/to/project", agent="build")
result = agent.run("Add type hints to utils.py and run the tests")
print(result.content)
agent.close()
# Via CLI (opencode must be installed)
jarvis ask --agent opencode "Refactor the parser to use a state machine"
Provider แบบ Pass-Through
หาก engine ไม่มี base URL ที่หาได้ ส่ง model เป็น provider/model (เช่น ollama/llama3) แล้ว opencode จะ resolve จาก configuration ของตัวเอง -- ไม่มีการเขียน opencode.json
ความสามารถของโมเดลสำคัญ
ลูปเชิงตัวแทนของ opencode (การวางแผน + การเรียก tool ที่ถูกต้อง + การทำงานต่อเนื่องหลายขั้นตอน) ต้องการโมเดลที่มีความสามารถพอสมควร ในการทดสอบ โมเดลในเครื่องขนาด 27B (Qwen3.5-27B ให้บริการผ่าน vLLM) แก้ชุดงานเขียนโค้ด 7 งานได้อย่างเรียบร้อย (สร้าง / แก้ไข / แก้บั๊ก / implement-ให้ผ่านเทสต์ / หลายไฟล์ ยืนยันด้วยการรันโค้ดและ เทสต์จริง) โมเดล 8B (qwen3:8b) ไม่น่าเชื่อถือ — เรียก tool ผิด รูปแบบ, โค้ดผิด syntax และงานทำไม่เสร็จครึ่งๆ กลางๆ เลือกใช้โมเดล ในเครื่องที่มีความสามารถ (หรือโมเดล cloud) สำหรับงานเขียนโค้ดจริง
OperativeAgent¶
OperativeAgent คือเอเจนต์อัตโนมัติแบบตั้งเวลาและต่อเนื่อง พร้อม session
persistence และการเรียกคืน state ในตัว ออกแบบมาสำหรับ "Operators" --
เอเจนต์อัตโนมัติที่รันตามตารางเวลา พร้อมการจัดการ state อัตโนมัติระหว่าง
tick Extend ToolUsingAgent
ทำงานอย่างไร:
- โหลด Session -- กู้คืนประวัติบทสนทนาจาก tick ก่อนหน้าผ่าน session store
- เรียกคืน State -- ดึง state JSON ก่อนหน้าจาก memory backend
- แทรก System Prompt -- แทรกคำสั่ง protocol ของ operator
- ลูป Tool -- ลูป function-calling มาตรฐาน (เหมือน OrchestratorAgent)
- บันทึก Session -- บันทึก prompt และคำตอบของ tick ลง session store
- การคงอยู่ของ State -- บันทึก state อัตโนมัติ หากเอเจนต์ไม่ได้เก็บมันเองผ่าน tool
memory_store
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool ที่จะเปิดให้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
20 |
จำนวนรอบการเรียกใช้ tool สูงสุด |
temperature |
float |
0.3 |
Temperature ของ sampling |
max_tokens |
int |
2048 |
จำนวน token สูงสุดที่จะสร้าง |
system_prompt |
str |
None |
System prompt กำหนดเองสำหรับ operator |
operator_id |
str |
None |
ID เฉพาะสำหรับ session และการคงอยู่ของ state |
session_store |
Any |
None |
Session store backend สำหรับประวัติบทสนทนา |
memory_backend |
Any |
None |
Memory backend สำหรับเรียกคืนและคงอยู่ของ state |
เมื่อไหร่ควรใช้: สำหรับเอเจนต์อัตโนมัติที่รันตามตารางเวลา (เช่น ผ่าน
TaskScheduler) และต้องคง state ไว้ข้ามการเรียกใช้ เอเจนต์จะจัดการประวัติ
session และการคงอยู่ของ state ข้าม tick โดยอัตโนมัติ
from openjarvis.agents.operative import OperativeAgent
agent = OperativeAgent(
engine,
model="qwen3:8b",
tools=[...],
operator_id="daily-report",
session_store=session_store,
memory_backend=memory_backend,
system_prompt="You are a daily report agent. Gather and summarize news.",
)
result = agent.run("Generate today's report")
MonitorOperativeAgent¶
MonitorOperativeAgent คือเอเจนต์ระยะยาวที่มีสี่แกนกลยุทธ์ที่ตั้งค่าได้
สำหรับจัดการข้อมูลข้ามรอบและ session มัน extend ToolUsingAgent ด้วยการ
บีบอัด observation, การดึงข้อมูล memory, การค้นหา และการแตกย่อยงานที่ขับ
เคลื่อนด้วยกลยุทธ์ นอกจากนี้ยังสืบทอด cross-session state persistence จาก
รูปแบบของ OperativeAgent ด้วย
แกนกลยุทธ์:
| แกน | ค่าที่ใช้ได้ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
memory_extraction |
causality_graph, scratchpad, structured_json, none |
causality_graph |
สิ่งที่พบจะถูกคงไว้ใน memory อย่างไร |
observation_compression |
summarize, truncate, none |
summarize |
ผลลัพธ์ tool ถูกบีบอัดอย่างไรก่อนถูกเพิ่มเข้าบริบท |
retrieval_strategy |
hybrid_with_self_eval, keyword, semantic, none |
hybrid_with_self_eval |
บริบทก่อนหน้าถูกเรียกคืนอย่างไรตอนเริ่มการรันแต่ละครั้ง |
task_decomposition |
phased, monolithic, hierarchical |
phased |
งานที่ซับซ้อนถูกแตกย่อยอย่างไร |
ทำงานอย่างไร:
- สร้าง system prompt พร้อมการตั้งค่ากลยุทธ์และคำอธิบาย tool
- เรียกคืน state ก่อนหน้าจาก memory backend
- โหลดประวัติ session จาก tick ก่อนหน้า
- รันลูป tool แบบ function-calling พร้อมใช้กลยุทธ์ที่ตั้งค่าไว้:
- การบีบอัด Observation: ผลลัพธ์ tool ที่ยาวจะถูกสรุป (ผ่าน LLM) หรือตัดให้สั้นลง ก่อนถูกเพิ่มเข้า message context
- การดึงข้อมูล Memory: หลังการเรียก tool แต่ละครั้ง สิ่งที่พบจะถูกดึงออกมาและเก็บไว้ตามกลยุทธ์ memory (ความสัมพันธ์เชิงเหตุผล, โน้ต scratchpad หรือ JSON แบบมีโครงสร้าง)
- บันทึก session และคงอยู่ของ state อัตโนมัติ
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
engine |
InferenceEngine |
-- | Inference engine ที่จะใช้ |
model |
str |
-- | Identifier ของโมเดล |
tools |
list[BaseTool] |
[] |
Instance ของ tool ที่จะเปิดให้ใช้ |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
max_turns |
int |
25 |
จำนวนรอบการเรียกใช้ tool สูงสุด |
temperature |
float |
0.3 |
Temperature ของ sampling |
max_tokens |
int |
4096 |
จำนวน token สูงสุดที่จะสร้าง |
system_prompt |
str |
None |
System prompt กำหนดเอง (override ค่าเริ่มต้น) |
memory_extraction |
str |
"causality_graph" |
กลยุทธ์การดึงข้อมูล memory |
observation_compression |
str |
"summarize" |
กลยุทธ์การบีบอัด observation |
retrieval_strategy |
str |
"hybrid_with_self_eval" |
กลยุทธ์การดึงข้อมูล |
task_decomposition |
str |
"phased" |
กลยุทธ์การแตกย่อยงาน |
operator_id |
str |
None |
ID เฉพาะสำหรับ session และการคงอยู่ของ state |
session_store |
Any |
None |
Session store backend สำหรับประวัติบทสนทนา |
memory_backend |
Any |
None |
Memory backend สำหรับ state และการคงอยู่ของสิ่งที่พบ |
เมื่อไหร่ควรใช้: สำหรับการประเมินผลแบบ long-horizon benchmark และงาน หลายขั้นตอนที่ซับซ้อน ซึ่งได้ประโยชน์จากกลยุทธ์ที่ตั้งค่าได้สำหรับการจัดการ memory, การบีบอัดบริบท และการแตกย่อยงาน มีประโยชน์เป็นพิเศษสำหรับ benchmark อย่าง GAIA, FRAMES และ LifelongAgent ที่การเลือกกลยุทธ์ส่งผลต่อประสิทธิภาพ
from openjarvis.agents.monitor_operative import MonitorOperativeAgent
agent = MonitorOperativeAgent(
engine,
model="qwen3:8b",
tools=[...],
operator_id="research-agent",
memory_extraction="causality_graph",
observation_compression="summarize",
retrieval_strategy="hybrid_with_self_eval",
task_decomposition="phased",
session_store=session_store,
memory_backend=memory_backend,
)
result = agent.run("Investigate the root cause of the production outage")
SandboxedAgent¶
SandboxedAgent คือ wrapper แบบโปร่งใสที่รัน BaseAgent ใดก็ได้ ภายใน
container ของ Docker (หรือ Podman) มันทำตามรูปแบบ wrapper เดียวกับ
GuardrailsEngine -- configuration ของเอเจนต์ภายในถูก serialize และส่งไปยัง
stdin ของ container แล้วอ่านผลลัพธ์กลับจาก stdout
ดูเอกสารอ้างอิง ContainerRunner ด้านล่างด้วย ซึ่ง
จัดการวงจรชีวิตของ container
ทำงานอย่างไร:
- สร้าง JSON payload พร้อม prompt, ID เอเจนต์ที่ห่อไว้ และโมเดล
- เรียก
ContainerRunner.run()ซึ่งเริ่ม container ด้วย--network noneและ--rmเขียน payload ลง stdin และรอผลลัพธ์ JSON ทาง stdout - Mount path ถูกตรวจสอบเทียบกับ allowlist ที่ตั้งค่าได้ ก่อนที่ container จะเริ่ม
- Parse ผลลัพธ์ที่คั่นด้วย sentinel และคืน
AgentResult
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
agent |
BaseAgent |
-- | เอเจนต์ที่ห่อไว้ ที่จะรันภายใน container |
runner |
ContainerRunner |
-- | Container runner จัดการวงจรชีวิตของ Docker |
engine |
InferenceEngine |
None |
Override engine (ค่าเริ่มต้นคือของเอเจนต์ที่ห่อไว้) |
model |
str |
"" |
Override model (ค่าเริ่มต้นคือของเอเจนต์ที่ห่อไว้) |
workspace |
str |
"" |
Working directory ภายใน container |
mounts |
list[str] |
[] |
Path ของ host ที่จะ bind-mount (อ่านอย่างเดียว) |
secrets |
dict[str, str] |
{} |
แทรกเข้า payload (ไม่ใช่ environment variable) |
bus |
EventBus |
None |
Event bus สำหรับ telemetry |
from openjarvis.sandbox import ContainerRunner, SandboxedAgent
from openjarvis.agents.simple import SimpleAgent
runner = ContainerRunner(
image="openjarvis-sandbox:latest",
timeout=60,
mount_allowlist_path="/etc/openjarvis/mount_allowlist.json",
)
inner = SimpleAgent(engine, model="qwen3:8b")
agent = SandboxedAgent(
agent=inner,
runner=runner,
mounts=["/home/user/data"],
)
result = agent.run("Summarize the CSV files in /home/user/data")
ContainerRunner¶
ContainerRunner จัดการวงจรชีวิตของ container Docker (หรือ Podman) สำหรับ
การรันแบบ sandbox SandboxedAgent ใช้มันโดยตรง แต่ใช้แบบ standalone ก็ได้
เช่นกัน
พารามิเตอร์ของ Constructor:
| พารามิเตอร์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
image |
str |
"openjarvis-sandbox:latest" |
Docker image ที่จะรัน |
timeout |
int |
300 |
เวลารัน container สูงสุดเป็นวินาที |
mount_allowlist_path |
str |
"" |
Path ไปยังไฟล์ mount-allowlist แบบ JSON |
max_concurrent |
int |
5 |
จำนวน container พร้อมกันสูงสุด (ระดับข้อมูล) |
runtime |
str |
"docker" |
Binary ของ container runtime (docker หรือ podman) |
รูปแบบ Mount Allowlist:
{
"roots": [
{"path": "/home/user/projects", "read_only": false},
{"path": "/data/shared", "read_only": true}
],
"blocked_patterns": [".ssh", ".env", "*.pem", "*.key"]
}
หากไม่ได้ตั้งค่า mount_allowlist_path จะไม่มีการจำกัด root ใดๆ
blocked_patterns มี .ssh, .env, *.pem, *.key, ไฟล์ credential และ
ไดเรกทอรี config ของ cloud รวมอยู่เป็นค่าเริ่มต้นเสมอ
ต้องมี Docker
ContainerRunner จะ raise RuntimeError หากไม่พบ runtime ที่ตั้งค่าไว้ (docker หรือ podman) ใน PATH
การลงทะเบียนเอเจนต์¶
เอเจนต์ลงทะเบียนผ่าน decorator @AgentRegistry.register() ทำให้ค้นพบได้
ด้วยชื่อตอน runtime:
from openjarvis.core.registry import AgentRegistry
# Check if an agent is registered
AgentRegistry.contains("orchestrator") # True
# Get the agent class
agent_cls = AgentRegistry.get("orchestrator")
# List all registered agent keys
AgentRegistry.keys()
# ["simple", "orchestrator", "native_react", "react", "native_openhands",
# "rlm", "openhands", "claude_code", "operative", "monitor_operative"]
การผสานกับ Event Bus¶
เอเจนต์ทั้งหมดเผยแพร่ event บน EventBus เมื่อระบุ bus ไว้:
| Event | เมื่อไหร่ |
|---|---|
AGENT_TURN_START |
ตอนเริ่มการรัน (ผ่าน _emit_turn_start) |
AGENT_TURN_END |
ตอนจบการรัน (ผ่าน _emit_turn_end) |
TOOL_CALL_START |
ก่อนการรัน tool แต่ละครั้ง (subclass ของ ToolUsingAgent) |
TOOL_CALL_END |
หลังการรัน tool แต่ละครั้ง (subclass ของ ToolUsingAgent) |
Event ของ Inference
Event INFERENCE_START / INFERENCE_END ถูกเผยแพร่โดย wrapper InstrumentedEngine ไม่ใช่โดยเอเจนต์โดยตรง วิธีนี้ทำให้ telemetry เป็น opt-in และโปร่งใสต่อโค้ดของเอเจนต์
Event เหล่านี้ทำให้ระบบ telemetry และ trace บันทึกข้อมูลการโต้ตอบแบบละเอียด ได้โดยอัตโนมัติ
Managed Agent Streaming¶
Managed Agent API (/v1/managed-agents/{id}/messages) รองรับ การสตรีม
token ของ LLM จริง ผ่าน SSE ส่งข้อความพร้อม stream: true เพื่อรับ token
คำตอบของโมเดลขณะที่มันถูกสร้างขึ้น แทนที่จะรอคำตอบแบบเต็ม
ทำงานอย่างไร¶
Endpoint การสตรีมเรียก engine.stream_full() โดยตรง ซึ่ง yield object
StreamChunk ที่มี content token, ชิ้นส่วนของ tool-call และเหตุผลการจบ
วิธีนี้ให้การสตรีมแบบ token-by-token จริงจาก LLM -- ไม่ใช่การ replay คำแบบ
ย้อนหลัง
สำหรับเอเจนต์แบบหลายรอบที่เรียกใช้ tool ลูปการสตรีมจะทำสิ่งเหล่านี้โดย อัตโนมัติ:
- Yield content token ให้ client ขณะที่มันมาถึง
- สะสมชิ้นส่วนของ tool-call (OpenAI ส่งสิ่งเหล่านี้แบบเพิ่มทีละส่วน)
- รัน tool เมื่อได้รับ
finish_reason="tool_calls" - ส่งผลลัพธ์ tool เป็น named SSE event (
event: tool_result) - ป้อนผลลัพธ์กลับให้ LLM สำหรับรอบถัดไป
- ทำซ้ำจนกว่าโมเดลจะสร้างคำตอบข้อความสุดท้าย หรือถึง
max_turns
สตรีมข้อความ¶
curl -N -X POST http://localhost:8000/v1/managed-agents/{id}/messages \
-H "Content-Type: application/json" \
-d '{"content": "What is 2+2?", "stream": true}'
คำตอบทำตามรูปแบบ SSE ของ OpenAI:
- Content chunk --
data: {"choices": [{"delta": {"content": "token"}}]} - Tool calls (ถ้าโมเดลขอใช้ tool) --
event: tool_calls\ndata: {"calls": [{"tool_name": "...", "arguments": "..."}]} - Tool results --
event: tool_result\ndata: {"tool_name": "...", "output": "..."} - Chunk สุดท้าย --
data: {"choices": [{"delta": {}, "finish_reason": "stop"}]} - Sentinel จบ --
data: [DONE]
เมื่อ stream: false (ค่าเริ่มต้น) endpoint จะทำงานเหมือนเดิมทุกประการ --
ข้อความถูกใส่คิวไว้และเอเจนต์ต้องถูก trigger แยกต่างหากผ่าน /run
รายละเอียดพฤติกรรม¶
- ข้อความของผู้ใช้จะถูกบันทึกลงฐานข้อมูลเสมอก่อนเริ่มสตรีม
- หลังการสตรีมเสร็จสิ้น คำตอบที่รวบรวมได้ทั้งหมดจะถูกบันทึกเป็นข้อความ
agent_to_user - ประวัติบทสนทนาจากข้อความก่อนหน้าจะถูกโหลดเป็นบริบท LLM โดยอัตโนมัติ
- Method
stream_full()ของ engine ถูกใช้สำหรับการสตรีม token จริง Engine ที่ไม่ได้ override มันจะ fallback ไปใช้ implementation เริ่มต้นที่ห่อ methodstream()ธรรมดา - หาก engine ไม่พร้อมใช้งานบนเซิร์ฟเวอร์ จะคืน error
503 - การรัน tool ระหว่างการสตรีมใช้
ToolRegistryเพื่อค้นหาและสร้าง instance ของ tool