emdash-core 0.1.7__py3-none-any.whl → 0.1.33__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.
- emdash_core/__init__.py +6 -1
- emdash_core/agent/__init__.py +4 -0
- emdash_core/agent/events.py +52 -1
- emdash_core/agent/inprocess_subagent.py +123 -10
- emdash_core/agent/prompts/__init__.py +6 -0
- emdash_core/agent/prompts/main_agent.py +53 -3
- emdash_core/agent/prompts/plan_mode.py +255 -0
- emdash_core/agent/prompts/subagents.py +84 -16
- emdash_core/agent/prompts/workflow.py +270 -56
- emdash_core/agent/providers/base.py +4 -0
- emdash_core/agent/providers/factory.py +2 -2
- emdash_core/agent/providers/models.py +7 -0
- emdash_core/agent/providers/openai_provider.py +137 -13
- emdash_core/agent/runner/__init__.py +49 -0
- emdash_core/agent/runner/agent_runner.py +753 -0
- emdash_core/agent/runner/context.py +451 -0
- emdash_core/agent/runner/factory.py +108 -0
- emdash_core/agent/runner/plan.py +217 -0
- emdash_core/agent/runner/sdk_runner.py +324 -0
- emdash_core/agent/runner/utils.py +67 -0
- emdash_core/agent/skills.py +358 -0
- emdash_core/agent/toolkit.py +85 -5
- emdash_core/agent/toolkits/plan.py +9 -11
- emdash_core/agent/tools/__init__.py +3 -2
- emdash_core/agent/tools/coding.py +48 -4
- emdash_core/agent/tools/modes.py +207 -55
- emdash_core/agent/tools/search.py +4 -0
- emdash_core/agent/tools/skill.py +193 -0
- emdash_core/agent/tools/spec.py +61 -94
- emdash_core/agent/tools/task.py +41 -2
- emdash_core/agent/tools/tasks.py +15 -78
- emdash_core/api/agent.py +562 -8
- emdash_core/api/index.py +1 -1
- emdash_core/api/projectmd.py +4 -2
- emdash_core/api/router.py +2 -0
- emdash_core/api/skills.py +241 -0
- emdash_core/checkpoint/__init__.py +40 -0
- emdash_core/checkpoint/cli.py +175 -0
- emdash_core/checkpoint/git_operations.py +250 -0
- emdash_core/checkpoint/manager.py +231 -0
- emdash_core/checkpoint/models.py +107 -0
- emdash_core/checkpoint/storage.py +201 -0
- emdash_core/config.py +1 -1
- emdash_core/core/config.py +18 -2
- emdash_core/graph/schema.py +5 -5
- emdash_core/ingestion/orchestrator.py +19 -10
- emdash_core/models/agent.py +1 -1
- emdash_core/server.py +42 -0
- emdash_core/skills/frontend-design/SKILL.md +56 -0
- emdash_core/sse/stream.py +5 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -2
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/RECORD +54 -37
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +1 -0
- emdash_core/agent/runner.py +0 -601
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import base64
|
|
5
|
+
import time
|
|
5
6
|
from typing import Optional, Union
|
|
6
7
|
|
|
7
8
|
from openai import OpenAI
|
|
@@ -30,6 +31,9 @@ PROVIDER_CONFIG = {
|
|
|
30
31
|
# Providers that support the reasoning parameter via extra_body
|
|
31
32
|
REASONING_SUPPORTED_PROVIDERS = {"openai"}
|
|
32
33
|
|
|
34
|
+
# Providers that support extended thinking
|
|
35
|
+
THINKING_SUPPORTED_PROVIDERS = {"anthropic"}
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
class OpenAIProvider(LLMProvider):
|
|
35
39
|
"""
|
|
@@ -66,9 +70,9 @@ class OpenAIProvider(LLMProvider):
|
|
|
66
70
|
self._context_limit = 128000
|
|
67
71
|
self._provider = self._infer_provider(model)
|
|
68
72
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
# Note: We no longer override provider based on OPENAI_BASE_URL
|
|
74
|
+
# Each provider (fireworks, anthropic) uses its own base_url
|
|
75
|
+
# OPENAI_BASE_URL only applies to "openai" provider
|
|
72
76
|
|
|
73
77
|
# Create OpenAI client with provider-specific configuration
|
|
74
78
|
config = PROVIDER_CONFIG.get(self._provider, PROVIDER_CONFIG["openai"])
|
|
@@ -131,10 +135,26 @@ class OpenAIProvider(LLMProvider):
|
|
|
131
135
|
)
|
|
132
136
|
|
|
133
137
|
self._reasoning_override = self._parse_bool_env("EMDASH_LLM_REASONING")
|
|
138
|
+
self._thinking_override = self._parse_bool_env("EMDASH_LLM_THINKING")
|
|
139
|
+
self._thinking_budget = int(os.environ.get("EMDASH_THINKING_BUDGET", "10000"))
|
|
140
|
+
# Reasoning effort for Fireworks thinking models: none, low, medium, high
|
|
141
|
+
self._reasoning_effort = os.environ.get("EMDASH_REASONING_EFFORT", "medium")
|
|
142
|
+
|
|
143
|
+
# Use OPENAI_BASE_URL env var only for OpenAI provider, otherwise use provider config
|
|
144
|
+
if self._provider == "openai":
|
|
145
|
+
base_url = os.environ.get("OPENAI_BASE_URL") or config["base_url"]
|
|
146
|
+
else:
|
|
147
|
+
base_url = config["base_url"]
|
|
148
|
+
|
|
149
|
+
# Configure timeout from environment (default 300 seconds / 5 minutes)
|
|
150
|
+
# LLM calls can take a while with large contexts, so we use a generous default
|
|
151
|
+
timeout_seconds = int(os.environ.get("EMDASH_LLM_TIMEOUT", "300"))
|
|
152
|
+
self._timeout = timeout_seconds
|
|
134
153
|
|
|
135
154
|
self.client = OpenAI(
|
|
136
155
|
api_key=api_key,
|
|
137
|
-
base_url=
|
|
156
|
+
base_url=base_url,
|
|
157
|
+
timeout=timeout_seconds,
|
|
138
158
|
)
|
|
139
159
|
|
|
140
160
|
@staticmethod
|
|
@@ -170,13 +190,10 @@ class OpenAIProvider(LLMProvider):
|
|
|
170
190
|
def _infer_provider(self, model: str) -> str:
|
|
171
191
|
"""Infer provider from model string.
|
|
172
192
|
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
Returns the appropriate provider based on model name.
|
|
194
|
+
OPENAI_BASE_URL only affects the openai provider's base URL,
|
|
195
|
+
not provider selection.
|
|
175
196
|
"""
|
|
176
|
-
# If custom base URL is set, use openai provider (uses OPENAI_API_KEY)
|
|
177
|
-
if os.environ.get("OPENAI_BASE_URL"):
|
|
178
|
-
return "openai"
|
|
179
|
-
|
|
180
197
|
model_lower = model.lower()
|
|
181
198
|
if "claude" in model_lower or "anthropic" in model_lower:
|
|
182
199
|
return "anthropic"
|
|
@@ -191,6 +208,7 @@ class OpenAIProvider(LLMProvider):
|
|
|
191
208
|
tools: Optional[list[dict]] = None,
|
|
192
209
|
system: Optional[str] = None,
|
|
193
210
|
reasoning: bool = False,
|
|
211
|
+
thinking: bool = False,
|
|
194
212
|
images: Optional[list[ImageContent]] = None,
|
|
195
213
|
) -> LLMResponse:
|
|
196
214
|
"""
|
|
@@ -201,6 +219,7 @@ class OpenAIProvider(LLMProvider):
|
|
|
201
219
|
tools: Optional list of tool schemas (OpenAI format)
|
|
202
220
|
system: Optional system prompt
|
|
203
221
|
reasoning: Enable reasoning mode (for models that support it)
|
|
222
|
+
thinking: Enable extended thinking (for Anthropic models)
|
|
204
223
|
images: Optional list of images for vision-capable models
|
|
205
224
|
|
|
206
225
|
Returns:
|
|
@@ -212,6 +231,8 @@ class OpenAIProvider(LLMProvider):
|
|
|
212
231
|
|
|
213
232
|
if self._reasoning_override is not None:
|
|
214
233
|
reasoning = self._reasoning_override
|
|
234
|
+
if self._thinking_override is not None:
|
|
235
|
+
thinking = self._thinking_override
|
|
215
236
|
|
|
216
237
|
# Build completion kwargs
|
|
217
238
|
kwargs = {
|
|
@@ -222,6 +243,7 @@ class OpenAIProvider(LLMProvider):
|
|
|
222
243
|
# Add tools if provided
|
|
223
244
|
if tools:
|
|
224
245
|
kwargs["tools"] = tools
|
|
246
|
+
kwargs["tool_choice"] = "auto"
|
|
225
247
|
|
|
226
248
|
# Add reasoning support via extra_body for providers that support it
|
|
227
249
|
# Skip reasoning for custom base URLs (they may not support it)
|
|
@@ -229,6 +251,33 @@ class OpenAIProvider(LLMProvider):
|
|
|
229
251
|
if reasoning and self._provider in REASONING_SUPPORTED_PROVIDERS and not is_custom_api:
|
|
230
252
|
kwargs["extra_body"] = {"reasoning": {"enabled": True}}
|
|
231
253
|
|
|
254
|
+
# Add extended thinking for Anthropic models
|
|
255
|
+
# This uses Anthropic's native thinking parameter
|
|
256
|
+
if thinking and self._provider in THINKING_SUPPORTED_PROVIDERS and not is_custom_api:
|
|
257
|
+
extra_body = kwargs.get("extra_body", {})
|
|
258
|
+
extra_body["thinking"] = {
|
|
259
|
+
"type": "enabled",
|
|
260
|
+
"budget_tokens": self._thinking_budget,
|
|
261
|
+
}
|
|
262
|
+
kwargs["extra_body"] = extra_body
|
|
263
|
+
log.info(
|
|
264
|
+
"Extended thinking enabled provider={} model={} budget={}",
|
|
265
|
+
self._provider,
|
|
266
|
+
self.model,
|
|
267
|
+
self._thinking_budget,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Add reasoning_effort for Fireworks thinking models
|
|
271
|
+
# This controls the depth of reasoning: none, low, medium, high
|
|
272
|
+
if thinking and self._provider == "fireworks" and self._reasoning_effort != "none":
|
|
273
|
+
kwargs["reasoning_effort"] = self._reasoning_effort
|
|
274
|
+
log.info(
|
|
275
|
+
"Reasoning effort enabled provider={} model={} effort={}",
|
|
276
|
+
self._provider,
|
|
277
|
+
self.model,
|
|
278
|
+
self._reasoning_effort,
|
|
279
|
+
)
|
|
280
|
+
|
|
232
281
|
# Add images if provided (vision support)
|
|
233
282
|
if images:
|
|
234
283
|
log.info(
|
|
@@ -287,21 +336,32 @@ class OpenAIProvider(LLMProvider):
|
|
|
287
336
|
)
|
|
288
337
|
|
|
289
338
|
# Call OpenAI SDK
|
|
339
|
+
start_time = time.time()
|
|
290
340
|
try:
|
|
291
341
|
response = self.client.chat.completions.create(**kwargs)
|
|
292
342
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
343
|
+
elapsed = time.time() - start_time
|
|
293
344
|
status = getattr(exc, "status_code", None)
|
|
294
345
|
code = getattr(exc, "code", None)
|
|
295
346
|
log.exception(
|
|
296
|
-
"LLM request failed provider={} model={} status={} code={} error={}",
|
|
347
|
+
"LLM request failed provider={} model={} status={} code={} elapsed={:.1f}s error={}",
|
|
297
348
|
self._provider,
|
|
298
349
|
self.model,
|
|
299
350
|
status,
|
|
300
351
|
code,
|
|
352
|
+
elapsed,
|
|
301
353
|
exc,
|
|
302
354
|
)
|
|
303
355
|
raise
|
|
304
356
|
|
|
357
|
+
elapsed = time.time() - start_time
|
|
358
|
+
log.info(
|
|
359
|
+
"LLM request completed provider={} model={} elapsed={:.1f}s",
|
|
360
|
+
self._provider,
|
|
361
|
+
self.model,
|
|
362
|
+
elapsed,
|
|
363
|
+
)
|
|
364
|
+
|
|
305
365
|
return self._to_llm_response(response)
|
|
306
366
|
|
|
307
367
|
def _to_llm_response(self, response) -> LLMResponse:
|
|
@@ -322,8 +382,42 @@ class OpenAIProvider(LLMProvider):
|
|
|
322
382
|
choice = response.choices[0]
|
|
323
383
|
message = choice.message
|
|
324
384
|
|
|
325
|
-
# Extract content
|
|
326
|
-
content =
|
|
385
|
+
# Extract content and thinking
|
|
386
|
+
content = None
|
|
387
|
+
thinking = None
|
|
388
|
+
|
|
389
|
+
# Check if content is a list of content blocks (Anthropic extended thinking)
|
|
390
|
+
raw_content = message.content
|
|
391
|
+
if isinstance(raw_content, list):
|
|
392
|
+
# Content blocks format (Anthropic with extended thinking)
|
|
393
|
+
text_parts = []
|
|
394
|
+
thinking_parts = []
|
|
395
|
+
for block in raw_content:
|
|
396
|
+
if hasattr(block, "type"):
|
|
397
|
+
if block.type == "thinking":
|
|
398
|
+
thinking_parts.append(getattr(block, "thinking", ""))
|
|
399
|
+
elif block.type == "text":
|
|
400
|
+
text_parts.append(getattr(block, "text", ""))
|
|
401
|
+
elif isinstance(block, dict):
|
|
402
|
+
if block.get("type") == "thinking":
|
|
403
|
+
thinking_parts.append(block.get("thinking", ""))
|
|
404
|
+
elif block.get("type") == "text":
|
|
405
|
+
text_parts.append(block.get("text", ""))
|
|
406
|
+
content = "\n".join(text_parts) if text_parts else None
|
|
407
|
+
thinking = "\n".join(thinking_parts) if thinking_parts else None
|
|
408
|
+
else:
|
|
409
|
+
# Simple string content
|
|
410
|
+
content = raw_content
|
|
411
|
+
|
|
412
|
+
# Check for reasoning_content field (Fireworks/OpenAI thinking models)
|
|
413
|
+
# This is separate from Anthropic's content blocks format
|
|
414
|
+
if not thinking and hasattr(message, "reasoning_content") and message.reasoning_content:
|
|
415
|
+
thinking = message.reasoning_content
|
|
416
|
+
log.debug(
|
|
417
|
+
"Reasoning content extracted from message.reasoning_content provider={} len={}",
|
|
418
|
+
self._provider,
|
|
419
|
+
len(thinking),
|
|
420
|
+
)
|
|
327
421
|
|
|
328
422
|
# Extract tool calls
|
|
329
423
|
tool_calls = []
|
|
@@ -338,17 +432,39 @@ class OpenAIProvider(LLMProvider):
|
|
|
338
432
|
# Extract token usage if available
|
|
339
433
|
input_tokens = 0
|
|
340
434
|
output_tokens = 0
|
|
435
|
+
thinking_tokens = 0
|
|
341
436
|
if hasattr(response, "usage") and response.usage:
|
|
342
437
|
input_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
|
|
343
438
|
output_tokens = getattr(response.usage, "completion_tokens", 0) or 0
|
|
439
|
+
# Try to get reasoning/thinking tokens from the API response
|
|
440
|
+
# Different providers use different field names
|
|
441
|
+
thinking_tokens = (
|
|
442
|
+
getattr(response.usage, "reasoning_tokens", 0)
|
|
443
|
+
or getattr(response.usage, "thinking_tokens", 0)
|
|
444
|
+
or 0
|
|
445
|
+
)
|
|
446
|
+
# If no explicit thinking tokens but we have thinking content, estimate
|
|
447
|
+
if not thinking_tokens and thinking:
|
|
448
|
+
thinking_tokens = len(thinking) // 4 # Rough estimate
|
|
449
|
+
|
|
450
|
+
if thinking:
|
|
451
|
+
log.info(
|
|
452
|
+
"Extended thinking captured provider={} model={} thinking_len={} thinking_tokens={}",
|
|
453
|
+
self._provider,
|
|
454
|
+
self.model,
|
|
455
|
+
len(thinking),
|
|
456
|
+
thinking_tokens,
|
|
457
|
+
)
|
|
344
458
|
|
|
345
459
|
return LLMResponse(
|
|
346
460
|
content=content,
|
|
461
|
+
thinking=thinking,
|
|
347
462
|
tool_calls=tool_calls,
|
|
348
463
|
raw=response,
|
|
349
464
|
stop_reason=choice.finish_reason,
|
|
350
465
|
input_tokens=input_tokens,
|
|
351
466
|
output_tokens=output_tokens,
|
|
467
|
+
thinking_tokens=thinking_tokens,
|
|
352
468
|
)
|
|
353
469
|
|
|
354
470
|
def get_context_limit(self) -> int:
|
|
@@ -373,6 +489,14 @@ class OpenAIProvider(LLMProvider):
|
|
|
373
489
|
# For unknown models, assume no vision support
|
|
374
490
|
return False
|
|
375
491
|
|
|
492
|
+
def supports_thinking(self) -> bool:
|
|
493
|
+
"""Check if this model supports extended thinking."""
|
|
494
|
+
if self.chat_model:
|
|
495
|
+
return self.chat_model.spec.supports_thinking
|
|
496
|
+
|
|
497
|
+
# For unknown models, check if provider supports thinking
|
|
498
|
+
return self._provider in THINKING_SUPPORTED_PROVIDERS
|
|
499
|
+
|
|
376
500
|
def _format_image_for_api(self, image: ImageContent) -> dict:
|
|
377
501
|
"""Format an image for OpenAI/Anthropic API.
|
|
378
502
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Agent runner module for LLM-powered exploration.
|
|
2
|
+
|
|
3
|
+
This module provides the AgentRunner class and related utilities for running
|
|
4
|
+
LLM agents with tool access for code exploration.
|
|
5
|
+
|
|
6
|
+
The module is organized as follows:
|
|
7
|
+
- agent_runner.py: Main AgentRunner class
|
|
8
|
+
- context.py: Context estimation, compaction, and management
|
|
9
|
+
- plan.py: Plan approval/rejection functionality
|
|
10
|
+
- utils.py: JSON encoding and utility functions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .agent_runner import AgentRunner
|
|
14
|
+
from .sdk_runner import SDKAgentRunner, is_claude_model
|
|
15
|
+
from .factory import get_runner, create_hybrid_runner
|
|
16
|
+
from .utils import SafeJSONEncoder, summarize_tool_result
|
|
17
|
+
from .context import (
|
|
18
|
+
estimate_context_tokens,
|
|
19
|
+
get_context_breakdown,
|
|
20
|
+
maybe_compact_context,
|
|
21
|
+
compact_messages_with_llm,
|
|
22
|
+
format_messages_for_summary,
|
|
23
|
+
get_reranked_context,
|
|
24
|
+
emit_context_frame,
|
|
25
|
+
)
|
|
26
|
+
from .plan import PlanMixin
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Main classes
|
|
30
|
+
"AgentRunner",
|
|
31
|
+
"SDKAgentRunner",
|
|
32
|
+
# Factory functions
|
|
33
|
+
"get_runner",
|
|
34
|
+
"create_hybrid_runner",
|
|
35
|
+
"is_claude_model",
|
|
36
|
+
# Utils
|
|
37
|
+
"SafeJSONEncoder",
|
|
38
|
+
"summarize_tool_result",
|
|
39
|
+
# Context functions
|
|
40
|
+
"estimate_context_tokens",
|
|
41
|
+
"get_context_breakdown",
|
|
42
|
+
"maybe_compact_context",
|
|
43
|
+
"compact_messages_with_llm",
|
|
44
|
+
"format_messages_for_summary",
|
|
45
|
+
"get_reranked_context",
|
|
46
|
+
"emit_context_frame",
|
|
47
|
+
# Plan management
|
|
48
|
+
"PlanMixin",
|
|
49
|
+
]
|