flowent 0.3.4 → 0.3.6

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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowent"
3
- version = "0.3.4"
3
+ version = "0.3.6"
4
4
  description = "A workflow orchestration platform for multi-agent collaboration"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -120,6 +120,8 @@ async def run_agent_stream(
120
120
  *,
121
121
  completion: CompletionCallable | None,
122
122
  connection: ProviderConnection,
123
+ conversation_recorder: Callable[[Sequence[Mapping[str, object]]], None]
124
+ | None = None,
123
125
  cwd: Path,
124
126
  messages: Sequence[Mapping[str, object]],
125
127
  extra_tool_runner: Callable[[str, dict[str, object]], Awaitable[ToolResult | None]]
@@ -252,6 +254,9 @@ async def run_agent_stream(
252
254
  if not tool_calls:
253
255
  if not final_content and not final_thinking:
254
256
  raise RuntimeError(EMPTY_MODEL_RESPONSE_ERROR)
257
+ conversation.append({"role": "assistant", "content": final_content})
258
+ if conversation_recorder is not None:
259
+ conversation_recorder([dict(message) for message in conversation])
255
260
  logger.info(
256
261
  "Agent response completed id=%s rounds=%s content_length=%s thinking_length=%s decision=final_response",
257
262
  assistant_id,
@@ -44,6 +44,7 @@ class AgentMcpManager(Protocol):
44
44
  @dataclass(frozen=True)
45
45
  class AgentRunResult:
46
46
  content: str
47
+ history: Sequence[Mapping[str, object]] = ()
47
48
  thinking: str = ""
48
49
 
49
50
 
@@ -124,6 +125,8 @@ class FlowentAgentRuntime:
124
125
  *,
125
126
  approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
126
127
  connection: ProviderConnection,
128
+ conversation_recorder: Callable[[Sequence[Mapping[str, object]]], None]
129
+ | None = None,
127
130
  context_compactor: Callable[
128
131
  [Sequence[Mapping[str, object]]], Awaitable[AgentContextUpdate | None]
129
132
  ]
@@ -179,6 +182,7 @@ class FlowentAgentRuntime:
179
182
  async for event in run_agent_stream(
180
183
  completion=self.chat_completion,
181
184
  connection=connection,
185
+ conversation_recorder=conversation_recorder,
182
186
  context_compactor=context_compactor,
183
187
  cwd=self.cwd,
184
188
  extra_tool_runner=extra_tool_runner,
@@ -197,13 +201,22 @@ class FlowentAgentRuntime:
197
201
  approval_transcript: Sequence[ApprovalTranscriptEntry] = (),
198
202
  connection: ProviderConnection,
199
203
  include_workflow_tools: bool = True,
204
+ history_start_index: int = 0,
200
205
  messages: Sequence[ChatMessage | Mapping[str, object]],
201
206
  user_request: str,
202
207
  workflow_depth: int = 0,
203
208
  ) -> AgentRunResult:
209
+ recorded_conversation: list[Mapping[str, object]] = []
210
+
211
+ def record_conversation(
212
+ conversation: Sequence[Mapping[str, object]],
213
+ ) -> None:
214
+ recorded_conversation[:] = [dict(message) for message in conversation]
215
+
204
216
  async for event in self.stream(
205
217
  approval_transcript=approval_transcript,
206
218
  connection=connection,
219
+ conversation_recorder=record_conversation,
207
220
  include_workflow_tools=include_workflow_tools,
208
221
  messages=messages,
209
222
  user_request=user_request,
@@ -215,6 +228,7 @@ class FlowentAgentRuntime:
215
228
  if isinstance(message, Mapping):
216
229
  return AgentRunResult(
217
230
  content=str(message.get("content") or ""),
231
+ history=recorded_conversation[history_start_index:],
218
232
  thinking=str(message.get("thinking") or ""),
219
233
  )
220
234
  raise RuntimeError("Agent did not return a final response.")
@@ -13,6 +13,14 @@ def migrate(connection: sqlite3.Connection) -> None:
13
13
  updated_at INTEGER NOT NULL DEFAULT (unixepoch())
14
14
  );
15
15
 
16
+ CREATE TABLE IF NOT EXISTS workflow_agent_histories (
17
+ workflow_id TEXT NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
18
+ node_id TEXT NOT NULL,
19
+ messages TEXT NOT NULL DEFAULT '[]',
20
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
21
+ PRIMARY KEY (workflow_id, node_id)
22
+ );
23
+
16
24
  CREATE TABLE IF NOT EXISTS mcp_servers (
17
25
  id TEXT PRIMARY KEY,
18
26
  name TEXT NOT NULL,
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import sqlite3
3
+ from collections.abc import Mapping
3
4
  from pathlib import Path
4
5
 
5
6
  from flowent.llm import ChatMessage, ReasoningEffort
@@ -172,6 +173,50 @@ class StateStore:
172
173
  with self.connect() as connection:
173
174
  return self._read_workflows(connection)
174
175
 
176
+ def read_workflow_agent_history(
177
+ self, workflow_id: str, node_id: str
178
+ ) -> list[dict[str, object]]:
179
+ with self.connect() as connection:
180
+ row = connection.execute(
181
+ """
182
+ SELECT messages
183
+ FROM workflow_agent_histories
184
+ WHERE workflow_id = ? AND node_id = ?
185
+ """,
186
+ (workflow_id, node_id),
187
+ ).fetchone()
188
+ if row is None:
189
+ return []
190
+ return workflow_agent_history_messages(row["messages"])
191
+
192
+ def save_workflow_agent_history(
193
+ self,
194
+ workflow_id: str,
195
+ node_id: str,
196
+ messages: list[Mapping[str, object]],
197
+ ) -> list[dict[str, object]]:
198
+ stored_messages = [dict(message) for message in messages]
199
+ with self.connect() as connection:
200
+ connection.execute(
201
+ """
202
+ INSERT INTO workflow_agent_histories (
203
+ workflow_id,
204
+ node_id,
205
+ messages
206
+ )
207
+ VALUES (?, ?, ?)
208
+ ON CONFLICT(workflow_id, node_id) DO UPDATE SET
209
+ messages = excluded.messages,
210
+ updated_at = unixepoch()
211
+ """,
212
+ (
213
+ workflow_id,
214
+ node_id,
215
+ json.dumps(stored_messages, ensure_ascii=False),
216
+ ),
217
+ )
218
+ return stored_messages
219
+
175
220
  def save_workflow(self, workflow: StoredWorkflow) -> StoredWorkflow:
176
221
  with self.connect() as connection:
177
222
  connection.execute(
@@ -1017,3 +1062,15 @@ class StateStore:
1017
1062
  """
1018
1063
  )
1019
1064
  ]
1065
+
1066
+
1067
+ def workflow_agent_history_messages(value: str) -> list[dict[str, object]]:
1068
+ parsed = json.loads(value or "[]")
1069
+ if not isinstance(parsed, list):
1070
+ raise ValueError("Workflow agent history must be a list.")
1071
+ messages: list[dict[str, object]] = []
1072
+ for item in parsed:
1073
+ if not isinstance(item, dict):
1074
+ raise ValueError("Workflow agent history messages must be objects.")
1075
+ messages.append(dict(item))
1076
+ return messages