agent-api-server 2.2.1a2__tar.gz → 2.2.1a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/PKG-INFO +2 -2
  2. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/openclaw_adapter/openclaw_adapter.py +235 -21
  3. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/thread.py +29 -7
  4. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/pyproject.toml +1 -1
  5. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/sdk/client.py +12 -6
  6. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/README.md +0 -0
  7. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/__init__.py +0 -0
  8. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/langgraph_adapter/__init__.py +0 -0
  9. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/langgraph_adapter/formatter.py +0 -0
  10. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/langgraph_adapter/langgraph_adapter.py +0 -0
  11. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/adapters/openclaw_adapter/__init__.py +0 -0
  12. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/agent_api_server.py +0 -0
  13. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/__init__.py +0 -0
  14. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/config.py +0 -0
  15. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/graph.py +0 -0
  16. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/router.py +0 -0
  17. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/api/schema.py +0 -0
  18. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/client/css/styles.css +0 -0
  19. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/client/favicon.ico +0 -0
  20. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/client/index.html +0 -0
  21. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/client/js/app.js +0 -0
  22. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/client/js/index.umd.js +0 -0
  23. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/__init__.py +0 -0
  24. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/config.py +0 -0
  25. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/crypto.py +0 -0
  26. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/formatting.py +0 -0
  27. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/logging.py +0 -0
  28. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/nats.py +0 -0
  29. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/postgres.py +0 -0
  30. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/common/redis.py +0 -0
  31. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/__init__.py +0 -0
  32. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/loader.py +0 -0
  33. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/__init__.py +0 -0
  34. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/agent_models.py +0 -0
  35. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/base_model.py +0 -0
  36. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/detect_message.py +0 -0
  37. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/dynamic_llm.py +0 -0
  38. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/input_normalization.py +0 -0
  39. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/middleware.py +0 -0
  40. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/model_info.py +0 -0
  41. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/schema_utils.py +0 -0
  42. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/model/streaming.py +0 -0
  43. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/runtime/__init__.py +0 -0
  44. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/runtime/base.py +0 -0
  45. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/core/runtime/manager.py +0 -0
  46. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/demo.py +0 -0
  47. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/integration/__init__.py +0 -0
  48. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/integration/listener.py +0 -0
  49. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/integration/registry.py +0 -0
  50. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/logging.json +0 -0
  51. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/sdk/__init__.py +0 -0
  52. {agent_api_server-2.2.1a2 → agent_api_server-2.2.1a4}/service.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: agent-api-server
3
- Version: 2.2.1a2
3
+ Version: 2.2.1a4
4
4
  Summary: A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot
5
5
  Keywords: fastapi,langgraph,agent,api-server
6
6
  Requires-Python: >=3.11,<3.14
@@ -3,6 +3,7 @@ import hashlib
3
3
  import json
4
4
  import logging
5
5
  import platform
6
+ import re
6
7
  import shlex
7
8
  import time
8
9
  from pathlib import Path
@@ -317,6 +318,10 @@ class OpenClawAgentAdapter(BaseAgentAdapter):
317
318
  self.session_name = self.settings.get("session_name", "main")
318
319
  self.query_field = self.settings.get("query_field")
319
320
  self.execution_settings = dict(self.settings.get("execution_options") or {})
321
+ self.run_completion_idle_timeout_seconds = self._resolve_run_completion_idle_timeout_seconds(
322
+ self.settings,
323
+ self.execution_settings,
324
+ )
320
325
  self.workspace = self._resolve_workspace(self.settings.get("workspace"))
321
326
  self.agent_config = dict(self.settings.get("agent_config") or {})
322
327
 
@@ -381,8 +386,11 @@ class OpenClawAgentAdapter(BaseAgentAdapter):
381
386
  session_name = self._resolve_session_name(context=context)
382
387
  try:
383
388
  agent = await self._load_agent(client, context=context)
384
- result = await agent.execute(
385
- self._resolve_query(inputs),
389
+ return await self._collect_run_result(
390
+ agent=agent,
391
+ query=self._resolve_query(inputs),
392
+ context=context,
393
+ session_name=session_name,
386
394
  options=self._build_execution_options(context),
387
395
  )
388
396
  except Exception as exc:
@@ -390,25 +398,6 @@ class OpenClawAgentAdapter(BaseAgentAdapter):
390
398
  finally:
391
399
  await client.close()
392
400
 
393
- if not result.success and result.error_message:
394
- raise RuntimeError(result.error_message)
395
-
396
- return ChatMessage(
397
- type="ai",
398
- content_type=detect_content_type(result.content),
399
- content=result.content,
400
- tool_calls=[self._normalize_tool_call(tool_call) for tool_call in result.tool_calls],
401
- response_metadata={
402
- "latency_ms": result.latency_ms,
403
- "stop_reason": result.stop_reason,
404
- "token_usage": serialize_data(result.token_usage),
405
- "completed_at": serialize_data(result.completed_at),
406
- "conversation_id": self._resolve_conversation_id(context),
407
- "session_name": session_name,
408
- },
409
- references=[],
410
- )
411
-
412
401
  async def stream(self, inputs: dict[str, Any], context: AgentExecutionContext) -> AsyncIterator[str]:
413
402
  client = await self._connect_client()
414
403
  content_parts: list[str] = []
@@ -693,6 +682,231 @@ class OpenClawAgentAdapter(BaseAgentAdapter):
693
682
  options["attachments"] = attachments
694
683
  return ExecutionOptions(**options)
695
684
 
685
+ async def _collect_run_result(
686
+ self,
687
+ *,
688
+ agent,
689
+ query: str,
690
+ context: AgentExecutionContext,
691
+ session_name: str,
692
+ options,
693
+ ) -> ChatMessage:
694
+ content_parts: list[str] = []
695
+ tool_calls: list[dict[str, Any]] = []
696
+ last_message: ChatMessage | None = None
697
+ last_real_tool_message: ChatMessage | None = None
698
+ event_iterator = agent.execute_stream_typed(query, options=options).__aiter__()
699
+
700
+ while True:
701
+ timeout = self.run_completion_idle_timeout_seconds if last_message is not None else None
702
+ try:
703
+ if timeout is None:
704
+ event = await anext(event_iterator)
705
+ else:
706
+ event = await asyncio.wait_for(anext(event_iterator), timeout=timeout)
707
+ except StopAsyncIteration:
708
+ break
709
+ except asyncio.TimeoutError:
710
+ logger.warning(
711
+ "OpenClaw run stream idle timeout without DoneEvent; agent=%s session=%s timeout_s=%s last_message_type=%s last_real_tool_message=%s",
712
+ self.agent_id,
713
+ session_name,
714
+ timeout,
715
+ last_message.type if last_message is not None else None,
716
+ last_real_tool_message is not None,
717
+ )
718
+ break
719
+
720
+ if isinstance(event, ContentEvent):
721
+ if event.text:
722
+ content_parts.append(event.text)
723
+ last_message = self._build_ai_message(
724
+ content="".join(content_parts),
725
+ tool_calls=tool_calls,
726
+ context=context,
727
+ session_name=session_name,
728
+ )
729
+ continue
730
+
731
+ if ThinkingEvent is not None and isinstance(event, ThinkingEvent):
732
+ continue
733
+
734
+ if isinstance(event, ToolCallEvent):
735
+ tool_calls = [*tool_calls, self._normalize_tool_call({"tool": event.tool, "input": event.input})]
736
+ last_message = self._build_ai_message(
737
+ content="".join(content_parts),
738
+ tool_calls=tool_calls,
739
+ context=context,
740
+ session_name=session_name,
741
+ )
742
+ continue
743
+
744
+ if isinstance(event, ToolResultEvent):
745
+ tool_message = self._build_tool_message(
746
+ output=event.output,
747
+ tool=event.tool,
748
+ duration_ms=event.duration_ms,
749
+ context=context,
750
+ session_name=session_name,
751
+ )
752
+ last_message = tool_message
753
+ if self._is_real_tool_message(tool_message):
754
+ last_real_tool_message = tool_message
755
+ continue
756
+
757
+ if isinstance(event, DoneEvent):
758
+ final_content = event.content or "".join(content_parts)
759
+ if final_content:
760
+ return self._build_ai_message(
761
+ content=final_content,
762
+ tool_calls=[],
763
+ context=context,
764
+ session_name=session_name,
765
+ extra_metadata={
766
+ "token_usage": serialize_data(event.token_usage),
767
+ "stop_reason": event.stop_reason,
768
+ },
769
+ )
770
+
771
+ return self._build_terminal_fallback_message(
772
+ context=context,
773
+ session_name=session_name,
774
+ tool_calls=tool_calls,
775
+ last_message=last_message,
776
+ last_real_tool_message=last_real_tool_message,
777
+ extra_metadata={"token_usage": serialize_data(event.token_usage), "stop_reason": event.stop_reason},
778
+ )
779
+
780
+ if isinstance(event, ErrorEvent):
781
+ raise RuntimeError(event.message)
782
+
783
+ return self._build_terminal_fallback_message(
784
+ context=context,
785
+ session_name=session_name,
786
+ tool_calls=tool_calls,
787
+ last_message=last_message,
788
+ last_real_tool_message=last_real_tool_message,
789
+ )
790
+
791
+ def _build_ai_message(
792
+ self,
793
+ *,
794
+ content: str,
795
+ tool_calls: list[dict[str, Any]],
796
+ context: AgentExecutionContext,
797
+ session_name: str,
798
+ extra_metadata: dict[str, Any] | None = None,
799
+ ) -> ChatMessage:
800
+ return ChatMessage(
801
+ type="ai",
802
+ content_type=detect_content_type(content),
803
+ content=content,
804
+ tool_calls=list(tool_calls),
805
+ response_metadata={
806
+ "conversation_id": self._resolve_conversation_id(context),
807
+ "session_name": session_name,
808
+ **(extra_metadata or {}),
809
+ },
810
+ references=[],
811
+ )
812
+
813
+ def _build_terminal_fallback_message(
814
+ self,
815
+ *,
816
+ context: AgentExecutionContext,
817
+ session_name: str,
818
+ tool_calls: list[dict[str, Any]],
819
+ last_message: ChatMessage | None,
820
+ last_real_tool_message: ChatMessage | None,
821
+ extra_metadata: dict[str, Any] | None = None,
822
+ ) -> ChatMessage:
823
+ if last_real_tool_message is not None:
824
+ return last_real_tool_message.model_copy(
825
+ update={"response_metadata": {**last_real_tool_message.response_metadata, **(extra_metadata or {})}}
826
+ )
827
+
828
+ if last_message is not None and last_message.type == "ai":
829
+ return last_message.model_copy(
830
+ update={"response_metadata": {**last_message.response_metadata, **(extra_metadata or {})}}
831
+ )
832
+
833
+ return self._build_ai_message(
834
+ content="",
835
+ tool_calls=tool_calls,
836
+ context=context,
837
+ session_name=session_name,
838
+ extra_metadata=extra_metadata,
839
+ )
840
+
841
+ @staticmethod
842
+ def _is_semantic_tool_message(message: ChatMessage) -> bool:
843
+ content = message.content.strip()
844
+ if not content:
845
+ return False
846
+ if message.content_type != "text":
847
+ return True
848
+
849
+ low_signal_patterns = (
850
+ r"^Successfully wrote \d+ bytes to .+",
851
+ r"^Wrote \d+ bytes to .+",
852
+ r"^File written to .+",
853
+ r"^Saved to .+",
854
+ )
855
+ return not any(re.match(pattern, content) for pattern in low_signal_patterns)
856
+
857
+ @staticmethod
858
+ def _is_real_tool_message(message: ChatMessage) -> bool:
859
+ content = message.content.strip()
860
+ if not OpenClawAgentAdapter._is_semantic_tool_message(message):
861
+ return False
862
+ if message.content_type != "text":
863
+ return True
864
+
865
+ low_value_status_patterns = (
866
+ r"^(done|ok|success|completed)\.?$",
867
+ r"^(analysis|task|job) (done|completed|finished)\.?$",
868
+ r"^(saved|written) successfully\.?$",
869
+ )
870
+ return not any(re.match(pattern, content, re.IGNORECASE) for pattern in low_value_status_patterns)
871
+
872
+ @staticmethod
873
+ def _resolve_run_completion_idle_timeout_seconds(
874
+ settings: dict[str, Any],
875
+ execution_settings: dict[str, Any],
876
+ ) -> float | None:
877
+ for source in (settings, execution_settings):
878
+ raw_value = source.get("run_completion_idle_timeout_seconds")
879
+ if raw_value is None:
880
+ continue
881
+ timeout = float(raw_value)
882
+ return timeout if timeout > 0 else None
883
+ return 5.0
884
+
885
+ def _build_tool_message(
886
+ self,
887
+ *,
888
+ output: str,
889
+ tool: str,
890
+ duration_ms: Any,
891
+ context: AgentExecutionContext,
892
+ session_name: str,
893
+ extra_metadata: dict[str, Any] | None = None,
894
+ ) -> ChatMessage:
895
+ return ChatMessage(
896
+ type="tool",
897
+ content_type=detect_content_type(output),
898
+ content=output,
899
+ tool_calls=[],
900
+ response_metadata={
901
+ "duration_ms": duration_ms,
902
+ "tool": tool,
903
+ "conversation_id": self._resolve_conversation_id(context),
904
+ "session_name": session_name,
905
+ **(extra_metadata or {}),
906
+ },
907
+ references=[],
908
+ )
909
+
696
910
  @staticmethod
697
911
  def _build_attachments(items: list[dict[str, Any]]):
698
912
  attachments = []
@@ -57,15 +57,34 @@ def _ensure_thread_ready(state: ThreadState, thread_id: str) -> None:
57
57
  )
58
58
 
59
59
 
60
+ def _thread_agent_conflict(thread_id: str, existing_graph_name: str, requested_graph_name: str) -> HTTPException:
61
+ return HTTPException(
62
+ status_code=status.HTTP_409_CONFLICT,
63
+ detail={
64
+ "error": "thread_agent_conflict",
65
+ "message": "Thread is already bound to a different agent",
66
+ "thread_id": thread_id,
67
+ "existing_graph_name": existing_graph_name,
68
+ "requested_graph_name": requested_graph_name,
69
+ },
70
+ )
71
+
72
+
73
+ def _ensure_thread_matches_graph(
74
+ state: ThreadState,
75
+ *,
76
+ thread_id: str,
77
+ requested_graph_name: str | None = None,
78
+ ) -> None:
79
+ if requested_graph_name and state.graph_name != requested_graph_name:
80
+ raise _thread_agent_conflict(thread_id, state.graph_name, requested_graph_name)
81
+
82
+
60
83
  async def _resolve_thread_for_execution(thread_id: str, request: Request) -> ThreadState:
61
84
  graph_name = request.headers.get("X-Agent-Name", "")
62
85
  state = await _get_or_create_thread(thread_id, graph_name)
63
86
  _ensure_thread_ready(state, thread_id)
64
-
65
- if graph_name:
66
- logger.info("Graph name overridden by header: %s", graph_name)
67
- state.graph_name = graph_name
68
-
87
+ _ensure_thread_matches_graph(state, thread_id=thread_id, requested_graph_name=graph_name or None)
69
88
  return state
70
89
 
71
90
 
@@ -114,6 +133,7 @@ async def _get_or_create_thread(thread_id: str, graph_name: Optional[str] = "")
114
133
  storage = AsyncRedisThreadStorage.get_worker_instance()
115
134
  thread_data = await storage.get_thread(thread_id)
116
135
  if thread_data:
136
+ _ensure_thread_matches_graph(thread_data, thread_id=thread_id, requested_graph_name=graph_name or None)
117
137
  return thread_data
118
138
 
119
139
  if not graph_name:
@@ -248,10 +268,12 @@ async def create_thread(graph_name: str, thread_id: Optional[str] = None) -> Thr
248
268
  if thread_id:
249
269
  try:
250
270
  state = await _get_existing_thread_or_404(thread_id)
271
+ _ensure_thread_matches_graph(state, thread_id=thread_id, requested_graph_name=graph_name)
251
272
  logger.info("%s already exists, returning existing thread", thread_id)
252
273
  return _thread_info(state)
253
- except HTTPException:
254
- pass
274
+ except HTTPException as exc:
275
+ if exc.status_code != status.HTTP_404_NOT_FOUND:
276
+ raise
255
277
 
256
278
  storage = None
257
279
  thread_state = None
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-api-server"
3
- version = "2.2.1-alpha2"
3
+ version = "2.2.1-alpha4"
4
4
  description = "A Langgraph agent API server that implements Langgraph agent's web capabilities and can interact with chatbot"
5
5
  authors = []
6
6
  readme = "README.md"
@@ -1,4 +1,5 @@
1
1
  from typing import Any, AsyncIterator, Iterator
2
+ from uuid import uuid4
2
3
 
3
4
  from core import AgentDefinition, AgentExecutionContext, AgentFramework, AgentManager, get_agent_manager
4
5
  from core import BaseAgentAdapter
@@ -177,6 +178,10 @@ class AgentSDK:
177
178
  await self.manager.get_definition(resolved_agent_name)
178
179
  return {}
179
180
 
181
+ @staticmethod
182
+ def _resolve_thread_id(thread_id: str | None) -> str:
183
+ return thread_id or f"sdk-{uuid4().hex}"
184
+
180
185
  @staticmethod
181
186
  def _build_context(
182
187
  agent_name: str,
@@ -201,7 +206,7 @@ class AgentSDK:
201
206
  self,
202
207
  agent_name: str,
203
208
  query: str | dict[str, Any],
204
- thread_id: str,
209
+ thread_id: str | None,
205
210
  ts_tenant: str | None,
206
211
  ei_token: str | None,
207
212
  attachments: list[dict[str, Any]] | None,
@@ -209,9 +214,10 @@ class AgentSDK:
209
214
  use_system_llm: bool | str | None,
210
215
  ) -> tuple[dict[str, Any], AgentExecutionContext]:
211
216
  inputs, normalized_attachments = normalize_agent_query(query, attachments)
217
+ resolved_thread_id = self._resolve_thread_id(thread_id)
212
218
  context = self._build_context(
213
219
  agent_name=agent_name,
214
- thread_id=thread_id,
220
+ thread_id=resolved_thread_id,
215
221
  ts_tenant=ts_tenant,
216
222
  ei_token=ei_token,
217
223
  attachments=normalized_attachments,
@@ -225,7 +231,7 @@ class AgentSDK:
225
231
  agent_name: str | dict[str, Any] | None = None,
226
232
  query: str | dict[str, Any] | None = None,
227
233
  *,
228
- thread_id: str = "sdk",
234
+ thread_id: str | None = None,
229
235
  ts_tenant: str | None = None,
230
236
  ei_token: str | None = None,
231
237
  attachments: list[dict[str, Any]] | None = None,
@@ -251,7 +257,7 @@ class AgentSDK:
251
257
  agent_name: str | dict[str, Any] | None = None,
252
258
  query: str | dict[str, Any] | None = None,
253
259
  *,
254
- thread_id: str = "sdk",
260
+ thread_id: str | None = None,
255
261
  ts_tenant: str | None = None,
256
262
  ei_token: str | None = None,
257
263
  attachments: list[dict[str, Any]] | None = None,
@@ -277,7 +283,7 @@ class AgentSDK:
277
283
  agent_name: str | dict[str, Any] | None = None,
278
284
  query: str | dict[str, Any] | None = None,
279
285
  *,
280
- thread_id: str = "sdk",
286
+ thread_id: str | None = None,
281
287
  ts_tenant: str | None = None,
282
288
  ei_token: str | None = None,
283
289
  attachments: list[dict[str, Any]] | None = None,
@@ -304,7 +310,7 @@ class AgentSDK:
304
310
  agent_name: str | dict[str, Any] | None = None,
305
311
  query: str | dict[str, Any] | None = None,
306
312
  *,
307
- thread_id: str = "sdk",
313
+ thread_id: str | None = None,
308
314
  ts_tenant: str | None = None,
309
315
  ei_token: str | None = None,
310
316
  attachments: list[dict[str, Any]] | None = None,