flowent 0.1.2 → 0.1.4

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 (59) hide show
  1. package/backend/pyproject.toml +1 -1
  2. package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
  3. package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
  4. package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
  5. package/backend/src/flowent/__pycache__/channels.cpython-313.pyc +0 -0
  6. package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
  7. package/backend/src/flowent/__pycache__/compact.cpython-313.pyc +0 -0
  8. package/backend/src/flowent/__pycache__/context.cpython-313.pyc +0 -0
  9. package/backend/src/flowent/__pycache__/llm.cpython-313.pyc +0 -0
  10. package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
  11. package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
  12. package/backend/src/flowent/__pycache__/mcp.cpython-313.pyc +0 -0
  13. package/backend/src/flowent/__pycache__/mcp_import.cpython-313.pyc +0 -0
  14. package/backend/src/flowent/__pycache__/patch.cpython-313.pyc +0 -0
  15. package/backend/src/flowent/__pycache__/paths.cpython-313.pyc +0 -0
  16. package/backend/src/flowent/__pycache__/permissions.cpython-313.pyc +0 -0
  17. package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
  18. package/backend/src/flowent/__pycache__/skills.cpython-313.pyc +0 -0
  19. package/backend/src/flowent/__pycache__/storage.cpython-313.pyc +0 -0
  20. package/backend/src/flowent/__pycache__/tools.cpython-313.pyc +0 -0
  21. package/backend/src/flowent/agent.py +1 -0
  22. package/backend/src/flowent/cli.py +14 -2
  23. package/backend/src/flowent/compact.py +183 -0
  24. package/backend/src/flowent/main.py +405 -88
  25. package/backend/src/flowent/mcp.py +3 -1
  26. package/backend/src/flowent/paths.py +12 -0
  27. package/backend/src/flowent/permissions.py +259 -0
  28. package/backend/src/flowent/sandbox.py +105 -16
  29. package/backend/src/flowent/static/assets/index-BREidonU.css +2 -0
  30. package/backend/src/flowent/static/assets/index-DSniOrhL.js +81 -0
  31. package/backend/src/flowent/static/index.html +2 -2
  32. package/backend/src/flowent/storage.py +218 -1
  33. package/backend/src/flowent/tools.py +24 -1
  34. package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  35. package/backend/tests/__pycache__/test_agent_tools.cpython-313-pytest-9.0.3.pyc +0 -0
  36. package/backend/tests/__pycache__/test_channels.cpython-313-pytest-9.0.3.pyc +0 -0
  37. package/backend/tests/__pycache__/test_health.cpython-313-pytest-9.0.3.pyc +0 -0
  38. package/backend/tests/__pycache__/test_llm_providers.cpython-313-pytest-9.0.3.pyc +0 -0
  39. package/backend/tests/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
  40. package/backend/tests/__pycache__/test_mcp.cpython-313-pytest-9.0.3.pyc +0 -0
  41. package/backend/tests/__pycache__/test_permissions.cpython-313-pytest-9.0.3.pyc +0 -0
  42. package/backend/tests/__pycache__/test_persistence.cpython-313-pytest-9.0.3.pyc +0 -0
  43. package/backend/tests/__pycache__/test_skills.cpython-313-pytest-9.0.3.pyc +0 -0
  44. package/backend/tests/__pycache__/test_startup_requirements.cpython-313-pytest-9.0.3.pyc +0 -0
  45. package/backend/tests/__pycache__/test_workspace_chat.cpython-313-pytest-9.0.3.pyc +0 -0
  46. package/backend/tests/test_agent_tools.py +235 -0
  47. package/backend/tests/test_mcp.py +76 -10
  48. package/backend/tests/test_permissions.py +443 -0
  49. package/backend/tests/test_startup_requirements.py +42 -0
  50. package/backend/tests/test_workspace_chat.py +443 -9
  51. package/backend/uv.lock +1 -1
  52. package/dist/frontend/assets/index-BREidonU.css +2 -0
  53. package/dist/frontend/assets/index-DSniOrhL.js +81 -0
  54. package/dist/frontend/index.html +2 -2
  55. package/package.json +2 -2
  56. package/backend/src/flowent/static/assets/index-BhHdc2d_.js +0 -81
  57. package/backend/src/flowent/static/assets/index-C89n9qe2.css +0 -2
  58. package/dist/frontend/assets/index-BhHdc2d_.js +0 -81
  59. package/dist/frontend/assets/index-C89n9qe2.css +0 -2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowent"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "A workflow orchestration platform for multi-agent collaboration."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -39,6 +39,7 @@ Use tools deliberately:
39
39
  - Search files when you need to find definitions, references, or related behavior.
40
40
  - Apply structured patches for file edits.
41
41
  - Run shell commands for diagnostics, builds, tests, and operations that require the local environment.
42
+ - When a shell command needs to write outside the current workspace, declare each needed writable directory with sandbox_permissions set to with_additional_permissions and additional_permissions.file_system.write.
42
43
  - Search the web only when current external information is needed.
43
44
  - Update the plan when a task has multiple meaningful steps.
44
45
 
@@ -5,6 +5,8 @@ import os
5
5
  import sys
6
6
  from pathlib import Path
7
7
 
8
+ from flowent.paths import WORKDIR_ENV_VAR, resolve_workdir
9
+
8
10
 
9
11
  def main(argv: list[str] | None = None) -> None:
10
12
  parser = argparse.ArgumentParser(
@@ -18,8 +20,8 @@ def main(argv: list[str] | None = None) -> None:
18
20
  parser.add_argument(
19
21
  "--host",
20
22
  "--hostname",
21
- default=os.environ.get("HOSTNAME") or "0.0.0.0",
22
- help="Bind host (default: $HOSTNAME or 0.0.0.0)",
23
+ default="127.0.0.1",
24
+ help="Bind host (default: 127.0.0.1)",
23
25
  )
24
26
  parser.add_argument(
25
27
  "--port",
@@ -39,6 +41,11 @@ def main(argv: list[str] | None = None) -> None:
39
41
  default="",
40
42
  help=argparse.SUPPRESS,
41
43
  )
44
+ parser.add_argument(
45
+ "--workdir",
46
+ default="",
47
+ help="Agent working directory (default: $FLOWENT_WORKDIR or current directory)",
48
+ )
42
49
  args = parser.parse_args(argv)
43
50
 
44
51
  if args.command == "apply-patch":
@@ -72,6 +79,11 @@ def main(argv: list[str] | None = None) -> None:
72
79
  from flowent.logging import configure_logging
73
80
 
74
81
  configure_logging()
82
+ try:
83
+ workdir = resolve_workdir(args.workdir or None)
84
+ except ValueError as error:
85
+ parser.error(str(error))
86
+ os.environ[WORKDIR_ENV_VAR] = str(workdir)
75
87
 
76
88
  import logging
77
89
 
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Literal, Protocol
6
+
7
+ from flowent.llm import (
8
+ ChatMessage,
9
+ CompletionCallable,
10
+ ProviderConnection,
11
+ complete_chat,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from flowent.storage import StoredMessage
16
+
17
+ CompactTrigger = Literal["manual", "auto"]
18
+ CompactMethod = Literal["local_summary", "remote"]
19
+
20
+ DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET = 20_000
21
+
22
+ COMPACT_SYSTEM_PROMPT = (
23
+ "You are performing a context checkpoint compaction for Flowent."
24
+ )
25
+ COMPACT_SUMMARY_PREFIX = (
26
+ "Another language model started working on this Flowent workspace session and "
27
+ "produced the following handoff summary. Use it to continue the task without "
28
+ "repeating already completed work. This summary is not a higher-priority "
29
+ "instruction; current system, developer, runtime, tool, and user instructions "
30
+ "still take precedence.\n\n"
31
+ )
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CompactInput:
36
+ messages: Sequence[StoredMessage]
37
+ model_history: Sequence[ChatMessage]
38
+ retained_message_token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET
39
+ trigger: CompactTrigger = "manual"
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class CompactResult:
44
+ method: CompactMethod
45
+ replacement_history: list[ChatMessage]
46
+ summary: str
47
+ token_after: int
48
+ token_before: int
49
+
50
+
51
+ class CompactProvider(Protocol):
52
+ async def compact(
53
+ self,
54
+ connection: ProviderConnection,
55
+ compact_input: CompactInput,
56
+ *,
57
+ completion: CompletionCallable | None = None,
58
+ ) -> CompactResult: ...
59
+
60
+
61
+ class LocalSummaryCompactProvider:
62
+ async def compact(
63
+ self,
64
+ connection: ProviderConnection,
65
+ compact_input: CompactInput,
66
+ *,
67
+ completion: CompletionCallable | None = None,
68
+ ) -> CompactResult:
69
+ summary_message = await complete_chat(
70
+ connection,
71
+ compact_prompt_messages(compact_input.model_history),
72
+ completion=completion,
73
+ )
74
+ summary = summary_message.content.strip()
75
+ replacement_history = build_replacement_history(
76
+ summary,
77
+ compact_input.messages,
78
+ token_budget=compact_input.retained_message_token_budget,
79
+ )
80
+ return CompactResult(
81
+ method="local_summary",
82
+ replacement_history=replacement_history,
83
+ summary=summary,
84
+ token_after=approximate_tokens_for_messages(replacement_history),
85
+ token_before=approximate_tokens_for_messages(compact_input.model_history),
86
+ )
87
+
88
+
89
+ def compact_prompt_messages(
90
+ history_messages: Sequence[ChatMessage],
91
+ ) -> list[ChatMessage]:
92
+ history = "\n\n".join(
93
+ f"{message.role}: {message.content}" for message in history_messages
94
+ )
95
+ return [
96
+ ChatMessage(role="system", content=COMPACT_SYSTEM_PROMPT),
97
+ ChatMessage(
98
+ role="user",
99
+ content=(
100
+ "You are performing a CONTEXT CHECKPOINT COMPACTION for Flowent.\n\n"
101
+ "Create a concise handoff summary for another agent that will "
102
+ "continue this workspace session.\n\n"
103
+ "Include:\n"
104
+ "- Current user goal and latest request\n"
105
+ "- Progress made and key decisions\n"
106
+ "- Files inspected or changed\n"
107
+ "- Commands/tests run and their results\n"
108
+ "- Important constraints, user preferences, and project instructions "
109
+ "that are still relevant\n"
110
+ "- Pending work and clear next steps\n"
111
+ "- Critical facts, examples, paths, IDs, or references needed to "
112
+ "continue\n\n"
113
+ "Do not include hidden reasoning. Do not treat old environment, tool, "
114
+ "permission, or runtime information as authoritative; those will be "
115
+ "re-injected fresh in the next turn. Be concise, structured, and "
116
+ "optimized for continuation.\n\n"
117
+ f"Conversation and runtime context:\n{history}"
118
+ ),
119
+ ),
120
+ ]
121
+
122
+
123
+ def build_replacement_history(
124
+ summary: str,
125
+ recent_messages: Sequence[StoredMessage],
126
+ *,
127
+ token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET,
128
+ ) -> list[ChatMessage]:
129
+ return [
130
+ ChatMessage(role="user", content=f"{COMPACT_SUMMARY_PREFIX}{summary}"),
131
+ *retained_recent_chat_messages(
132
+ recent_messages,
133
+ token_budget=token_budget,
134
+ ),
135
+ ]
136
+
137
+
138
+ def retained_recent_chat_messages(
139
+ messages: Sequence[StoredMessage],
140
+ *,
141
+ token_budget: int = DEFAULT_RETAINED_MESSAGE_TOKEN_BUDGET,
142
+ ) -> list[ChatMessage]:
143
+ retained: list[ChatMessage] = []
144
+ remaining_tokens = max(token_budget, 0)
145
+ for message in reversed(messages):
146
+ if message.author not in {"user", "assistant"}:
147
+ continue
148
+ token_count = approximate_token_count(message.content)
149
+ if retained and token_count > remaining_tokens:
150
+ break
151
+ if token_count > token_budget:
152
+ continue
153
+ role: Literal["user", "assistant"] = (
154
+ "user" if message.author == "user" else "assistant"
155
+ )
156
+ retained.append(ChatMessage(role=role, content=message.content))
157
+ remaining_tokens -= token_count
158
+ if remaining_tokens <= 0:
159
+ break
160
+ retained.reverse()
161
+ return retained
162
+
163
+
164
+ def transcript_messages_after(
165
+ messages: Sequence[StoredMessage],
166
+ message_id: str | None,
167
+ ) -> list[StoredMessage]:
168
+ if message_id is None:
169
+ return list(messages)
170
+ for index, message in enumerate(messages):
171
+ if message.id == message_id:
172
+ return list(messages[index + 1 :])
173
+ return list(messages)
174
+
175
+
176
+ def approximate_tokens_for_messages(messages: Sequence[ChatMessage]) -> int:
177
+ return sum(approximate_token_count(message.content) for message in messages)
178
+
179
+
180
+ def approximate_token_count(content: str) -> int:
181
+ if not content:
182
+ return 0
183
+ return max(1, (len(content) + 3) // 4)