minima-cli 0.4.9__py3-none-any.whl

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 (161) hide show
  1. minima/__init__.py +5 -0
  2. minima/api/__init__.py +1 -0
  3. minima/api/auth.py +39 -0
  4. minima/api/errors.py +40 -0
  5. minima/api/routers/__init__.py +1 -0
  6. minima/api/routers/calibration.py +50 -0
  7. minima/api/routers/feedback.py +279 -0
  8. minima/api/routers/health.py +50 -0
  9. minima/api/routers/models.py +42 -0
  10. minima/api/routers/recommend.py +66 -0
  11. minima/api/routers/savings.py +55 -0
  12. minima/api/routers/strategies.py +33 -0
  13. minima/catalog/__init__.py +1 -0
  14. minima/catalog/data/capability_priors.json +210 -0
  15. minima/catalog/data/model_aliases.json +12 -0
  16. minima/catalog/merge.py +69 -0
  17. minima/catalog/refresh.py +54 -0
  18. minima/catalog/sources/__init__.py +1 -0
  19. minima/catalog/sources/litellm.py +19 -0
  20. minima/catalog/sources/openrouter.py +25 -0
  21. minima/catalog/store.py +86 -0
  22. minima/config.py +288 -0
  23. minima/deps.py +35 -0
  24. minima/llm/__init__.py +1 -0
  25. minima/llm/anthropic.py +106 -0
  26. minima/llm/base.py +196 -0
  27. minima/llm/gemini.py +124 -0
  28. minima/llm/registry.py +54 -0
  29. minima/logging.py +28 -0
  30. minima/main.py +109 -0
  31. minima/memory/__init__.py +1 -0
  32. minima/memory/adapter.py +572 -0
  33. minima/memory/keys.py +83 -0
  34. minima/memory/records.py +190 -0
  35. minima/memory/threadpool.py +41 -0
  36. minima/metrics/__init__.py +1 -0
  37. minima/metrics/calibration.py +415 -0
  38. minima/metrics/report.py +116 -0
  39. minima/metrics/savings.py +98 -0
  40. minima/recommender/__init__.py +1 -0
  41. minima/recommender/_pg_pool.py +38 -0
  42. minima/recommender/_redis_client.py +32 -0
  43. minima/recommender/aggregate.py +157 -0
  44. minima/recommender/classify.py +165 -0
  45. minima/recommender/decisionlog.py +505 -0
  46. minima/recommender/durablerefs.py +312 -0
  47. minima/recommender/engine.py +997 -0
  48. minima/recommender/escalation.py +83 -0
  49. minima/recommender/propensity.py +189 -0
  50. minima/recommender/recstore.py +368 -0
  51. minima/recommender/score.py +318 -0
  52. minima/recommender/types.py +166 -0
  53. minima/schemas/__init__.py +1 -0
  54. minima/schemas/common.py +73 -0
  55. minima/schemas/feedback.py +34 -0
  56. minima/schemas/models_catalog.py +36 -0
  57. minima/schemas/recommend.py +104 -0
  58. minima/schemas/savings.py +39 -0
  59. minima/schemas/strategies.py +57 -0
  60. minima/schemas/workflow.py +43 -0
  61. minima/seeding/__init__.py +1 -0
  62. minima/seeding/items.py +42 -0
  63. minima/seeding/llmrouterbench.py +232 -0
  64. minima/seeding/routerbench.py +141 -0
  65. minima/seeding/run_seed.py +56 -0
  66. minima/seeding/synthetic.py +70 -0
  67. minima/tenancy/__init__.py +8 -0
  68. minima/tenancy/context.py +37 -0
  69. minima/tenancy/passthrough.py +110 -0
  70. minima/version.py +3 -0
  71. minima_cli-0.4.9.dist-info/METADATA +275 -0
  72. minima_cli-0.4.9.dist-info/RECORD +161 -0
  73. minima_cli-0.4.9.dist-info/WHEEL +4 -0
  74. minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
  75. minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
  76. minima_client/__init__.py +19 -0
  77. minima_client/autocapture.py +101 -0
  78. minima_client/client.py +301 -0
  79. minima_client/errors.py +23 -0
  80. minima_harness/LICENSE_PI +32 -0
  81. minima_harness/__init__.py +16 -0
  82. minima_harness/agent/__init__.py +72 -0
  83. minima_harness/agent/agent.py +276 -0
  84. minima_harness/agent/events.py +124 -0
  85. minima_harness/agent/loop.py +311 -0
  86. minima_harness/agent/state.py +79 -0
  87. minima_harness/agent/tools.py +97 -0
  88. minima_harness/ai/__init__.py +66 -0
  89. minima_harness/ai/compat.py +71 -0
  90. minima_harness/ai/errors.py +96 -0
  91. minima_harness/ai/events.py +117 -0
  92. minima_harness/ai/openrouter_catalog.py +153 -0
  93. minima_harness/ai/provider_catalog.py +299 -0
  94. minima_harness/ai/provider_quirks.py +37 -0
  95. minima_harness/ai/providers/__init__.py +75 -0
  96. minima_harness/ai/providers/_common.py +48 -0
  97. minima_harness/ai/providers/anthropic.py +290 -0
  98. minima_harness/ai/providers/base.py +65 -0
  99. minima_harness/ai/providers/faux.py +173 -0
  100. minima_harness/ai/providers/google.py +221 -0
  101. minima_harness/ai/providers/openai_compat.py +278 -0
  102. minima_harness/ai/registry.py +184 -0
  103. minima_harness/ai/stream.py +82 -0
  104. minima_harness/ai/tools.py +51 -0
  105. minima_harness/ai/types.py +204 -0
  106. minima_harness/ai/usage.py +41 -0
  107. minima_harness/minima/__init__.py +40 -0
  108. minima_harness/minima/cache.py +102 -0
  109. minima_harness/minima/config.py +85 -0
  110. minima_harness/minima/goals.py +226 -0
  111. minima_harness/minima/judge.py +144 -0
  112. minima_harness/minima/mapping.py +147 -0
  113. minima_harness/minima/meter.py +143 -0
  114. minima_harness/minima/router.py +220 -0
  115. minima_harness/minima/runtime.py +544 -0
  116. minima_harness/minima/signals.py +195 -0
  117. minima_harness/session/__init__.py +14 -0
  118. minima_harness/session/format.py +35 -0
  119. minima_harness/session/store.py +236 -0
  120. minima_harness/tasks/__init__.py +17 -0
  121. minima_harness/tasks/task_set.py +78 -0
  122. minima_harness/tools/__init__.py +7 -0
  123. minima_harness/tools/_io.py +34 -0
  124. minima_harness/tools/bash.py +70 -0
  125. minima_harness/tools/builtin.py +23 -0
  126. minima_harness/tools/edit.py +50 -0
  127. minima_harness/tools/find.py +38 -0
  128. minima_harness/tools/grep.py +73 -0
  129. minima_harness/tools/ls.py +35 -0
  130. minima_harness/tools/read.py +38 -0
  131. minima_harness/tools/tasks.py +75 -0
  132. minima_harness/tools/write.py +36 -0
  133. minima_harness/tui/__init__.py +3 -0
  134. minima_harness/tui/analytics.py +111 -0
  135. minima_harness/tui/app.py +1927 -0
  136. minima_harness/tui/bridge.py +103 -0
  137. minima_harness/tui/cli.py +227 -0
  138. minima_harness/tui/clipboard.py +60 -0
  139. minima_harness/tui/commands.py +49 -0
  140. minima_harness/tui/compaction.py +17 -0
  141. minima_harness/tui/config_cli.py +141 -0
  142. minima_harness/tui/config_store.py +237 -0
  143. minima_harness/tui/context.py +93 -0
  144. minima_harness/tui/customize.py +95 -0
  145. minima_harness/tui/diff.py +53 -0
  146. minima_harness/tui/editor.py +43 -0
  147. minima_harness/tui/extensions.py +84 -0
  148. minima_harness/tui/extra_models.py +52 -0
  149. minima_harness/tui/history.py +71 -0
  150. minima_harness/tui/mubit.py +295 -0
  151. minima_harness/tui/overlays.py +593 -0
  152. minima_harness/tui/packages.py +59 -0
  153. minima_harness/tui/run_modes.py +66 -0
  154. minima_harness/tui/theme.py +77 -0
  155. minima_harness/tui/welcome.py +83 -0
  156. minima_harness/tui/widgets/__init__.py +3 -0
  157. minima_harness/tui/widgets/banner.py +38 -0
  158. minima_harness/tui/widgets/editor.py +83 -0
  159. minima_harness/tui/widgets/footer.py +73 -0
  160. minima_harness/tui/widgets/messages.py +151 -0
  161. minima_harness/tui/widgets/status.py +57 -0
@@ -0,0 +1,276 @@
1
+ """The stateful Agent — a port of PI's ``pi-agent-core`` ``Agent`` class.
2
+
3
+ Wraps :func:`agent_loop` with: persistent state across prompts, ordered awaited
4
+ subscribers, abort via an anyio cancel scope, and steering/follow-up queues injected
5
+ between turns. ``prompt()`` awaits the full run inline (matching PI); for background use
6
+ launch it in a task and await :meth:`wait_for_idle`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import inspect
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ import anyio
17
+
18
+ from minima_harness.agent.events import AgentEvent
19
+ from minima_harness.agent.loop import agent_loop
20
+ from minima_harness.agent.state import (
21
+ AgentLoopConfig,
22
+ AgentState,
23
+ ConvertToLlm,
24
+ TransformContext,
25
+ default_convert_to_llm,
26
+ )
27
+ from minima_harness.agent.tools import (
28
+ AfterToolCall,
29
+ BeforeToolCall,
30
+ ThinkingLevel,
31
+ ToolExecutionMode,
32
+ )
33
+ from minima_harness.ai.types import ContentBlock, Message, Model
34
+
35
+ Listener = Callable[[AgentEvent], Any]
36
+
37
+
38
+ class Agent:
39
+ def __init__(
40
+ self,
41
+ *,
42
+ model: Model,
43
+ system_prompt: str | None = None,
44
+ tools: list | None = None,
45
+ messages: list[Message] | None = None,
46
+ thinking_level: ThinkingLevel = "off",
47
+ convert_to_llm: ConvertToLlm = default_convert_to_llm,
48
+ transform_context: TransformContext | None = None,
49
+ tool_execution: ToolExecutionMode = "parallel",
50
+ before_tool_call: BeforeToolCall | None = None,
51
+ after_tool_call: AfterToolCall | None = None,
52
+ thinking_budgets: dict[str, int] | None = None,
53
+ max_turns: int = 50,
54
+ session_id: str | None = None,
55
+ stream_options: dict[str, Any] | None = None,
56
+ steering_mode: str = "one-at-a-time",
57
+ follow_up_mode: str = "one-at-a-time",
58
+ should_stop_after_turn: Any = None,
59
+ ) -> None:
60
+ self._state = AgentState(
61
+ system_prompt=system_prompt,
62
+ model=model,
63
+ thinking_level=thinking_level,
64
+ tools=list(tools or []),
65
+ messages=list(messages or []),
66
+ steering_mode=steering_mode,
67
+ follow_up_mode=follow_up_mode,
68
+ )
69
+ self._convert_to_llm = convert_to_llm
70
+ self._transform_context = transform_context
71
+ self._tool_execution = tool_execution
72
+ self._before_tool_call = before_tool_call
73
+ self._after_tool_call = after_tool_call
74
+ self._thinking_budgets = thinking_budgets
75
+ self._max_turns = max_turns
76
+ self._session_id = session_id
77
+ self._stream_options = stream_options
78
+ self._should_stop_after_turn = should_stop_after_turn
79
+ self._listeners: list[Listener] = []
80
+ self._cancel_scope: anyio.CancelScope | None = None
81
+ self._idle = asyncio.Event()
82
+ self._idle.set()
83
+
84
+ # ----------------------------------------------------------------- state
85
+
86
+ @property
87
+ def state(self) -> AgentState:
88
+ return self._state
89
+
90
+ def reset(self) -> None:
91
+ """Clear the conversation + error; keep model, tools, system prompt."""
92
+ self._state.messages = []
93
+ self._state.streaming_message = None
94
+ self._state.error_message = None
95
+ self._state.pending_tool_calls.clear()
96
+
97
+ # ----------------------------------------------- mutators (PI-style attrs)
98
+
99
+ @property
100
+ def tool_execution(self) -> ToolExecutionMode:
101
+ return self._tool_execution
102
+
103
+ @tool_execution.setter
104
+ def tool_execution(self, value: ToolExecutionMode) -> None:
105
+ self._tool_execution = value
106
+
107
+ @property
108
+ def before_tool_call(self) -> BeforeToolCall | None:
109
+ return self._before_tool_call
110
+
111
+ @before_tool_call.setter
112
+ def before_tool_call(self, value: BeforeToolCall | None) -> None:
113
+ self._before_tool_call = value
114
+
115
+ @property
116
+ def after_tool_call(self) -> AfterToolCall | None:
117
+ return self._after_tool_call
118
+
119
+ @after_tool_call.setter
120
+ def after_tool_call(self, value: AfterToolCall | None) -> None:
121
+ self._after_tool_call = value
122
+
123
+ @property
124
+ def session_id(self) -> str | None:
125
+ return self._session_id
126
+
127
+ @session_id.setter
128
+ def session_id(self, value: str | None) -> None:
129
+ self._session_id = value
130
+
131
+ @property
132
+ def thinking_budgets(self) -> dict[str, int] | None:
133
+ return self._thinking_budgets
134
+
135
+ @thinking_budgets.setter
136
+ def thinking_budgets(self, value: dict[str, int] | None) -> None:
137
+ self._thinking_budgets = value
138
+
139
+ # ------------------------------------------------------------- subscribe
140
+
141
+ def subscribe(self, listener: Listener) -> Callable[[], None]:
142
+ """Register a listener (sync or async). Returns an unsubscribe callable."""
143
+ self._listeners.append(listener)
144
+
145
+ def _unsubscribe() -> None:
146
+ try:
147
+ self._listeners.remove(listener)
148
+ except ValueError:
149
+ pass
150
+
151
+ return _unsubscribe
152
+
153
+ async def _dispatch(self, event: AgentEvent) -> None:
154
+ for listener in list(self._listeners):
155
+ result = listener(event)
156
+ if inspect.isawaitable(result):
157
+ await result
158
+
159
+ # ----------------------------------------------------------- prompting
160
+
161
+ async def prompt(self, content: str | list[ContentBlock] | Message | list[Any]) -> None:
162
+ """Run the loop with ``content`` appended as a user turn. Awaits completion."""
163
+ await self._run(self._coerce_prompts(content))
164
+
165
+ async def continue_(self) -> None:
166
+ """Resume from current context without a new user message."""
167
+ if self._state.messages and self._state.messages[-1].role == "assistant":
168
+ raise ValueError("continue_() requires the last message to be user or toolResult")
169
+ await self._run([])
170
+
171
+ async def _run(self, prompts: list[Message]) -> None:
172
+ if self._state.is_streaming:
173
+ raise RuntimeError("agent is already running")
174
+ self._idle.clear()
175
+ self._state.is_streaming = True
176
+ self._state.error_message = None
177
+ scope = anyio.CancelScope()
178
+ try:
179
+ with scope:
180
+ self._cancel_scope = scope
181
+ config = self._build_config()
182
+ async for event in agent_loop(prompts, self._state, config):
183
+ await self._dispatch(event)
184
+ if scope.cancelled_caught:
185
+ if self._state.error_message is None:
186
+ self._state.error_message = "aborted"
187
+ finally:
188
+ self._cancel_scope = None
189
+ self._state.is_streaming = False
190
+ self._state.streaming_message = None
191
+ self._idle.set()
192
+
193
+ def abort(self) -> None:
194
+ """Cancel the in-flight run (if any). No-op when idle."""
195
+ if self._cancel_scope is not None:
196
+ self._cancel_scope.cancel()
197
+
198
+ async def wait_for_idle(self) -> None:
199
+ """Await the current run's completion (for background-task usage)."""
200
+ await self._idle.wait()
201
+
202
+ # ----------------------------------------------------- steering/follow-up
203
+
204
+ def steer(self, message: Message | str) -> None:
205
+ self._state.steering.append(self._as_message(message))
206
+
207
+ def follow_up(self, message: Message | str) -> None:
208
+ self._state.follow_up.append(self._as_message(message))
209
+
210
+ def clear_steering_queue(self) -> None:
211
+ self._state.steering.clear()
212
+
213
+ def clear_follow_up_queue(self) -> None:
214
+ self._state.follow_up.clear()
215
+
216
+ def clear_all_queues(self) -> None:
217
+ self.clear_steering_queue()
218
+ self.clear_follow_up_queue()
219
+
220
+ @property
221
+ def steering_mode(self) -> str:
222
+ return self._state.steering_mode
223
+
224
+ @steering_mode.setter
225
+ def steering_mode(self, value: str) -> None:
226
+ self._state.steering_mode = value
227
+
228
+ @property
229
+ def follow_up_mode(self) -> str:
230
+ return self._state.follow_up_mode
231
+
232
+ @follow_up_mode.setter
233
+ def follow_up_mode(self, value: str) -> None:
234
+ self._state.follow_up_mode = value
235
+
236
+ # --------------------------------------------------------------- internals
237
+
238
+ def _build_config(self) -> AgentLoopConfig:
239
+ assert self._state.model is not None
240
+ return AgentLoopConfig(
241
+ model=self._state.model,
242
+ convert_to_llm=self._convert_to_llm,
243
+ tool_execution=self._tool_execution,
244
+ before_tool_call=self._before_tool_call,
245
+ after_tool_call=self._after_tool_call,
246
+ transform_context=self._transform_context,
247
+ should_stop_after_turn=self._should_stop_after_turn,
248
+ thinking_budgets=self._thinking_budgets,
249
+ thinking_level=self._state.thinking_level,
250
+ max_turns=self._max_turns,
251
+ session_id=self._session_id,
252
+ stream_options=self._stream_options,
253
+ )
254
+
255
+ @staticmethod
256
+ def _as_message(message: Message | str) -> Message:
257
+ if isinstance(message, Message):
258
+ return message
259
+ return Message(role="user", content=message)
260
+
261
+ def _coerce_prompts(self, content: Any) -> list[Message]:
262
+ if isinstance(content, Message):
263
+ return [content]
264
+ if isinstance(content, str):
265
+ return [Message(role="user", content=content)]
266
+ if isinstance(content, list):
267
+ prompts: list[Message] = []
268
+ for item in content:
269
+ if isinstance(item, Message):
270
+ prompts.append(item)
271
+ elif isinstance(item, str):
272
+ prompts.append(Message(role="user", content=item))
273
+ else:
274
+ prompts.append(Message(role="user", content=item))
275
+ return prompts
276
+ return [Message(role="user", content=content)]
@@ -0,0 +1,124 @@
1
+ """Agent runtime events — a port of PI's ``pi-agent-core`` event taxonomy.
2
+
3
+ Emitted by :func:`minima_harness.agent.loop.agent_loop` in a strict order per turn::
4
+
5
+ agent_start
6
+ (per turn)
7
+ turn_start
8
+ message_start {user or toolResult}
9
+ message_end {...}
10
+ message_start {assistant}
11
+ message_update {assistant_message_event: a provider StreamEvent}
12
+ message_end {assistant}
13
+ tool_execution_start / tool_execution_update / tool_execution_end (if toolUse)
14
+ message_start {toolResult} / message_end
15
+ turn_end
16
+ agent_end
17
+
18
+ Events are immutable dataclasses so they can be fanned out to many subscribers safely.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+ from typing import TYPE_CHECKING, Any, Literal
25
+
26
+ from minima_harness.ai.events import Event as StreamEvent
27
+
28
+ if TYPE_CHECKING:
29
+ pass
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class AgentStartEvent:
34
+ type: Literal["agent_start"] = "agent_start"
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class AgentEndEvent:
39
+ type: Literal["agent_end"] = "agent_end"
40
+ messages: list = field(default_factory=list) # list[Message]
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class TurnStartEvent:
45
+ type: Literal["turn_start"] = "turn_start"
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class TurnEndEvent:
50
+ type: Literal["turn_end"] = "turn_end"
51
+ message: Any = None # AssistantMessage | None
52
+ tool_results: list = field(default_factory=list) # list[ToolResult]
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class MessageStartEvent:
57
+ type: Literal["message_start"] = "message_start"
58
+ message: Any = None # Message | None
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class MessageUpdateEvent:
63
+ """Assistant-only. Wraps a provider streaming event (text/thinking/toolcall delta)."""
64
+
65
+ type: Literal["message_update"] = "message_update"
66
+ assistant_message_event: StreamEvent | None = None
67
+
68
+
69
+ @dataclass(frozen=True, slots=True)
70
+ class MessageEndEvent:
71
+ type: Literal["message_end"] = "message_end"
72
+ message: Any = None # Message | None
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class ToolExecutionStartEvent:
77
+ type: Literal["tool_execution_start"] = "tool_execution_start"
78
+ tool_call_id: str = ""
79
+ tool_name: str = ""
80
+ args: Any = None # validated params (pydantic model) | None when blocked/invalid
81
+
82
+
83
+ @dataclass(frozen=True, slots=True)
84
+ class ToolExecutionUpdateEvent:
85
+ type: Literal["tool_execution_update"] = "tool_execution_update"
86
+ tool_call_id: str = ""
87
+ partial: Any = None
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class ToolExecutionEndEvent:
92
+ type: Literal["tool_execution_end"] = "tool_execution_end"
93
+ tool_call_id: str = ""
94
+ result: Any = None # ToolResult | None
95
+ is_error: bool = False
96
+
97
+
98
+ AgentEvent = (
99
+ AgentStartEvent
100
+ | AgentEndEvent
101
+ | TurnStartEvent
102
+ | TurnEndEvent
103
+ | MessageStartEvent
104
+ | MessageUpdateEvent
105
+ | MessageEndEvent
106
+ | ToolExecutionStartEvent
107
+ | ToolExecutionUpdateEvent
108
+ | ToolExecutionEndEvent
109
+ )
110
+
111
+
112
+ __all__ = [
113
+ "AgentEndEvent",
114
+ "AgentEvent",
115
+ "AgentStartEvent",
116
+ "MessageEndEvent",
117
+ "MessageStartEvent",
118
+ "MessageUpdateEvent",
119
+ "ToolExecutionEndEvent",
120
+ "ToolExecutionStartEvent",
121
+ "ToolExecutionUpdateEvent",
122
+ "TurnEndEvent",
123
+ "TurnStartEvent",
124
+ ]