agentnova 0.3.7__tar.gz → 0.3.9__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.
- {agentnova-0.3.7 → agentnova-0.3.9}/PKG-INFO +7 -8
- {agentnova-0.3.7 → agentnova-0.3.9}/README.md +7 -8
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/__init__.py +1 -1
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/acp_plugin.py +7 -3
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/agent.py +260 -32
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/backends/ollama.py +77 -40
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/cli.py +107 -84
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/colors.py +203 -203
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/__init__.py +7 -7
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/helpers.py +76 -23
- agentnova-0.3.9/agentnova/core/model_config.py +36 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/model_family_config.py +51 -2
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/models.py +6 -1
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/prompts.py +14 -1
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/tool_cache.py +223 -188
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/tool_parse.py +46 -43
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/loader.py +5 -2
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/SKILL.md +1 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/soul/loader.py +12 -4
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/souls/nova-helper/SOUL.md +29 -1
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/tools/builtins.py +1 -9
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/tools/registry.py +1 -1
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/tools/sandboxed_repl.py +4 -5
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/PKG-INFO +7 -8
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/SOURCES.txt +2 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/pyproject.toml +1 -1
- agentnova-0.3.9/tests/test_builtins.py +349 -0
- agentnova-0.3.9/tests/test_security.py +406 -0
- agentnova-0.3.7/agentnova/core/model_config.py +0 -239
- {agentnova-0.3.7 → agentnova-0.3.9}/LICENSE +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/__main__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/agent_mode.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/backends/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/backends/base.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/backends/bitnet.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/config.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/args_normal.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/error_recovery.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/math_prompts.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/memory.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/openresponses.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/core/types.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/00_basic_agent.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/01_quick_diagnostic.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/02_tool_test.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/03_reasoning_test.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/04_gsm8k_benchmark.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/05_common_sense.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/06_causal_reasoning.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/07_logical_deduction.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/08_reading_comprehension.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/09_general_knowledge.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/10_implicit_reasoning.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/11_analogical_reasoning.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/examples/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/model_discovery.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/orchestrator.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/py.typed +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/shared_args.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/acp/SKILL.md +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/datetime/SKILL.md +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/generate_report.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/improve_description.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/init_skill.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/package_skill.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/quick_validate.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/run_eval.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/run_loop.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/security_scan.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/test_package_skill.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/test_quick_validate.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/utils.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/skill-creator/scripts/validate.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/skills/web-search/SKILL.md +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/soul/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/soul/types.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/souls/nova-helper/IDENTITY.md +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/souls/nova-helper/STYLE.md +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/souls/nova-helper/soul.json +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova/tools/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/dependency_links.txt +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/entry_points.txt +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/requires.txt +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/agentnova.egg-info/top_level.txt +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/localclaw/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/localclaw-redirect/localclaw/__init__.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/setup.cfg +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/tests/test_agent.py +0 -0
- {agentnova-0.3.7 → agentnova-0.3.9}/tests/test_spec_compliance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentnova
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: ⚛️ AgentNova - A minimal, hackable agentic framework for local LLM inference
|
|
5
5
|
Author-email: VTSTech <veritas@vts-tech.org>
|
|
6
6
|
Maintainer-email: VTSTech <veritas@vts-tech.org>
|
|
@@ -32,7 +32,7 @@ Requires-Dist: black>=23.0; extra == "dev"
|
|
|
32
32
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
33
33
|
Dynamic: license-file
|
|
34
34
|
|
|
35
|
-
# ⚛️ AgentNova R03.
|
|
35
|
+
# ⚛️ AgentNova R03.9
|
|
36
36
|
|
|
37
37
|
**Status: Alpha**
|
|
38
38
|
|
|
@@ -49,7 +49,8 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
|
|
|
49
49
|
|
|
50
50
|
[](#license) [](https://python.org)
|
|
51
51
|
|
|
52
|
-
<img width="
|
|
52
|
+
<img width="1314" height="993" alt="image" src="https://github.com/user-attachments/assets/586b1a44-441e-41ae-85c2-b65b6a0816b9" />
|
|
53
|
+
<img width="1063" height="574" alt="image" src="https://github.com/user-attachments/assets/eab6f4ad-810c-4741-b637-e120f9ccb974" />
|
|
53
54
|
|
|
54
55
|
## 📚 Documentation
|
|
55
56
|
|
|
@@ -76,12 +77,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
|
|
|
76
77
|
## Installation
|
|
77
78
|
|
|
78
79
|
```bash
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
cd AgentNova
|
|
82
|
-
pip install -e .
|
|
80
|
+
# Latest Development Release
|
|
81
|
+
pip install git+https://github.com/VTSTech/AgentNova.git --force-reinstall
|
|
83
82
|
|
|
84
|
-
#
|
|
83
|
+
# Last Stable (as stable as Alpha can be) Release
|
|
85
84
|
pip install agentnova
|
|
86
85
|
```
|
|
87
86
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ⚛️ AgentNova R03.
|
|
1
|
+
# ⚛️ AgentNova R03.9
|
|
2
2
|
|
|
3
3
|
**Status: Alpha**
|
|
4
4
|
|
|
@@ -15,7 +15,8 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
|
|
|
15
15
|
|
|
16
16
|
[](#license) [](https://python.org)
|
|
17
17
|
|
|
18
|
-
<img width="
|
|
18
|
+
<img width="1314" height="993" alt="image" src="https://github.com/user-attachments/assets/586b1a44-441e-41ae-85c2-b65b6a0816b9" />
|
|
19
|
+
<img width="1063" height="574" alt="image" src="https://github.com/user-attachments/assets/eab6f4ad-810c-4741-b637-e120f9ccb974" />
|
|
19
20
|
|
|
20
21
|
## 📚 Documentation
|
|
21
22
|
|
|
@@ -42,12 +43,10 @@ Inspired by the architecture of OpenClaw, rebuilt from scratch for local-first o
|
|
|
42
43
|
## Installation
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
cd AgentNova
|
|
48
|
-
pip install -e .
|
|
46
|
+
# Latest Development Release
|
|
47
|
+
pip install git+https://github.com/VTSTech/AgentNova.git --force-reinstall
|
|
49
48
|
|
|
50
|
-
#
|
|
49
|
+
# Last Stable (as stable as Alpha can be) Release
|
|
51
50
|
pip install agentnova
|
|
52
51
|
```
|
|
53
52
|
|
|
@@ -349,4 +348,4 @@ Contributions welcome! Please read the contributing guidelines first.
|
|
|
349
348
|
|
|
350
349
|
- Built for local inference with [Ollama](https://ollama.ai)
|
|
351
350
|
- Optimized for small, efficient models
|
|
352
|
-
- Inspired by ReAct and other agentic frameworks
|
|
351
|
+
- Inspired by ReAct and other agentic frameworks
|
|
@@ -281,6 +281,10 @@ class ACPPlugin:
|
|
|
281
281
|
"get_note": "READ",
|
|
282
282
|
"list_notes": "READ",
|
|
283
283
|
"list_directory": "READ",
|
|
284
|
+
# ACP v1.0.4: A2A agent-to-agent communication
|
|
285
|
+
"a2a_send": "A2A",
|
|
286
|
+
"a2a_request": "A2A",
|
|
287
|
+
"a2a_response": "A2A",
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
# v1.0.4: Auto-derive capabilities from tool action map if not explicitly provided
|
|
@@ -1089,11 +1093,11 @@ class ACPPlugin:
|
|
|
1089
1093
|
"""Format tool arguments as a target string for ACP."""
|
|
1090
1094
|
# Tool-specific formatting
|
|
1091
1095
|
if tool_name == "read_file":
|
|
1092
|
-
return args.get("path", "unknown")
|
|
1096
|
+
return args.get("file_path", args.get("path", "unknown"))
|
|
1093
1097
|
elif tool_name == "write_file":
|
|
1094
|
-
return args.get("path", "unknown")
|
|
1098
|
+
return args.get("file_path", args.get("path", "unknown"))
|
|
1095
1099
|
elif tool_name == "edit_file":
|
|
1096
|
-
return args.get("path", "unknown")
|
|
1100
|
+
return args.get("file_path", args.get("path", "unknown"))
|
|
1097
1101
|
elif tool_name == "shell":
|
|
1098
1102
|
return args.get("command", "")[:100]
|
|
1099
1103
|
elif tool_name == "calculator":
|
|
@@ -40,6 +40,7 @@ from .core.openresponses import (
|
|
|
40
40
|
MessageItem, FunctionCallItem, FunctionCallOutputItem, ReasoningItem,
|
|
41
41
|
OutputText, InputText,
|
|
42
42
|
RequestConfig, Error,
|
|
43
|
+
EventType, ResponseEvent, OutputItemEvent,
|
|
43
44
|
create_message_item, create_function_call_item, create_function_call_output,
|
|
44
45
|
create_function_call_output_item,
|
|
45
46
|
stream_response_events,
|
|
@@ -132,7 +133,7 @@ class Agent:
|
|
|
132
133
|
system_prompt: Custom system prompt (overrides soul)
|
|
133
134
|
soul: Path to Soul Spec package (default: "nova-helper")
|
|
134
135
|
soul_level: Progressive disclosure level for soul (1-3)
|
|
135
|
-
num_ctx: Context window size in tokens (default:
|
|
136
|
+
num_ctx: Context window size in tokens (default: 8192)
|
|
136
137
|
temperature: Sampling temperature (default: model-specific)
|
|
137
138
|
top_p: Nucleus sampling probability (default: model-specific)
|
|
138
139
|
num_predict: Maximum tokens to generate (default: model-specific)
|
|
@@ -143,12 +144,12 @@ class Agent:
|
|
|
143
144
|
self.model = model
|
|
144
145
|
self.max_steps = max_steps
|
|
145
146
|
self.debug = debug
|
|
146
|
-
# Get num_ctx from: explicit param > config/env > default
|
|
147
|
+
# Get num_ctx from: explicit param > config/env > default 8192
|
|
147
148
|
if num_ctx is not None:
|
|
148
149
|
self.num_ctx = num_ctx
|
|
149
150
|
else:
|
|
150
151
|
config = get_config()
|
|
151
|
-
self.num_ctx = config.num_ctx if config.num_ctx else
|
|
152
|
+
self.num_ctx = config.num_ctx if config.num_ctx else 8192
|
|
152
153
|
|
|
153
154
|
# Generation parameters (use model defaults if not specified)
|
|
154
155
|
self._temperature = temperature
|
|
@@ -225,7 +226,7 @@ class Agent:
|
|
|
225
226
|
self.memory = Memory(memory_config or MemoryConfig())
|
|
226
227
|
|
|
227
228
|
# Get model configuration (for temperature, max_tokens defaults)
|
|
228
|
-
from .core.
|
|
229
|
+
from .core.model_family_config import get_model_config
|
|
229
230
|
self.model_config = get_model_config(model)
|
|
230
231
|
|
|
231
232
|
# Detect model family (for backend-specific settings like think=False)
|
|
@@ -413,8 +414,11 @@ Final Answer: <the answer>
|
|
|
413
414
|
# No synthesis or fallback mechanisms are used.
|
|
414
415
|
|
|
415
416
|
# Track when we're expecting a Final Answer (after successful tool use)
|
|
417
|
+
# Only enforced for terminal tools (calculator, get_time, etc.) to avoid
|
|
418
|
+
# breaking multi-step workflows that need multiple tool calls.
|
|
416
419
|
_expecting_final_answer = False
|
|
417
420
|
_last_successful_result = None
|
|
421
|
+
_last_tool_name = None
|
|
418
422
|
|
|
419
423
|
# Reset error tracker for new run
|
|
420
424
|
self._error_tracker.reset()
|
|
@@ -441,6 +445,31 @@ Final Answer: <the answer>
|
|
|
441
445
|
tokens = gen_response.get("usage", {}).get("total_tokens", 0)
|
|
442
446
|
total_tokens += tokens
|
|
443
447
|
|
|
448
|
+
# OpenResponses: Handle finish_reason from backend
|
|
449
|
+
finish_reason = gen_response.get("_finish_reason", "stop")
|
|
450
|
+
if finish_reason == "length":
|
|
451
|
+
# Token budget exhausted — response is incomplete
|
|
452
|
+
if self.debug:
|
|
453
|
+
print(f" [OpenResponses] finish_reason='length' — marking incomplete")
|
|
454
|
+
steps.append(StepResult(
|
|
455
|
+
type=StepResultType.MAX_STEPS,
|
|
456
|
+
content="Response truncated: token limit reached",
|
|
457
|
+
tokens_used=tokens,
|
|
458
|
+
))
|
|
459
|
+
response.mark_incomplete()
|
|
460
|
+
break
|
|
461
|
+
elif finish_reason == "content_filter":
|
|
462
|
+
# Content was filtered — response failed
|
|
463
|
+
if self.debug:
|
|
464
|
+
print(f" [OpenResponses] finish_reason='content_filter' — marking failed")
|
|
465
|
+
steps.append(StepResult(
|
|
466
|
+
type=StepResultType.ERROR,
|
|
467
|
+
error="Response blocked by content filter",
|
|
468
|
+
tokens_used=tokens,
|
|
469
|
+
))
|
|
470
|
+
response.mark_failed({"message": "Content filtered by provider", "type": "content_filter"})
|
|
471
|
+
break
|
|
472
|
+
|
|
444
473
|
if self.debug:
|
|
445
474
|
print(f" Content: {content[:200] if content else '(empty)'}...")
|
|
446
475
|
print(f" Native tool calls: {native_tool_calls}")
|
|
@@ -558,9 +587,6 @@ Final Answer: <the answer>
|
|
|
558
587
|
self.memory.add("user", f"Observation: Error: {error_msg}")
|
|
559
588
|
continue
|
|
560
589
|
|
|
561
|
-
# Fuzzy match tool name
|
|
562
|
-
tool_name = self._parser._fuzzy_match_tool(tool_name)
|
|
563
|
-
|
|
564
590
|
# Create FunctionCallItem
|
|
565
591
|
fc_item = create_function_call_item(tool_name, tool_args, tool_call_id)
|
|
566
592
|
fc_item.status = ItemStatus.IN_PROGRESS
|
|
@@ -590,6 +616,9 @@ Final Answer: <the answer>
|
|
|
590
616
|
fc_item.status = ItemStatus.FAILED
|
|
591
617
|
response.mark_failed({"message": "Too many tool failures", "type": "error_recovery"})
|
|
592
618
|
break
|
|
619
|
+
else:
|
|
620
|
+
# Record success to reset consecutive failure counter
|
|
621
|
+
self._error_tracker.record_success(tool_name)
|
|
593
622
|
|
|
594
623
|
# Update FunctionCallItem status
|
|
595
624
|
fc_item.status = ItemStatus.COMPLETED
|
|
@@ -622,15 +651,25 @@ Final Answer: <the answer>
|
|
|
622
651
|
)
|
|
623
652
|
|
|
624
653
|
# Update expecting_final_answer flag
|
|
654
|
+
# Only enforce for terminal tools (simple, direct-answer tools)
|
|
655
|
+
# to avoid breaking multi-step workflows
|
|
625
656
|
if is_error:
|
|
626
657
|
_expecting_final_answer = False
|
|
658
|
+
_last_tool_name = None
|
|
627
659
|
else:
|
|
628
|
-
|
|
629
|
-
|
|
660
|
+
from .core.error_recovery import _is_simple_result
|
|
661
|
+
if _is_simple_result(str(result), tool_name):
|
|
662
|
+
_expecting_final_answer = True
|
|
663
|
+
_last_successful_result = str(result)
|
|
664
|
+
_last_tool_name = tool_name
|
|
665
|
+
else:
|
|
666
|
+
# Complex/intermediate result — allow more tool calls
|
|
667
|
+
_expecting_final_answer = False
|
|
668
|
+
_last_tool_name = None
|
|
630
669
|
|
|
631
670
|
self.memory.add("user", observation_msg)
|
|
632
671
|
|
|
633
|
-
if not
|
|
672
|
+
if not is_error:
|
|
634
673
|
successful_results.append(f"{tool_name}: {result}")
|
|
635
674
|
|
|
636
675
|
steps.append(StepResult(
|
|
@@ -873,6 +912,10 @@ Final Answer: <the answer>
|
|
|
873
912
|
OpenResponses specification. It yields Server-Sent Events (SSE) that
|
|
874
913
|
describe the response lifecycle and content deltas.
|
|
875
914
|
|
|
915
|
+
IMPORTANT: The agentic loop is fully supported during streaming.
|
|
916
|
+
When the model produces a tool call, it is executed and the loop
|
|
917
|
+
continues, streaming the next model response.
|
|
918
|
+
|
|
876
919
|
SSE Event Sequence (per OpenResponses spec):
|
|
877
920
|
1. response.queued - Response is queued
|
|
878
921
|
2. response.in_progress - Response started
|
|
@@ -895,6 +938,8 @@ Final Answer: <the answer>
|
|
|
895
938
|
for sse_event in agent.run_stream("Hello!"):
|
|
896
939
|
print(sse_event) # SSE formatted event
|
|
897
940
|
"""
|
|
941
|
+
start_time = time.time()
|
|
942
|
+
|
|
898
943
|
# Create OpenResponses Response object
|
|
899
944
|
response = Response(
|
|
900
945
|
model=self.model,
|
|
@@ -903,9 +948,8 @@ Final Answer: <the answer>
|
|
|
903
948
|
allowed_tools=self._allowed_tools or [],
|
|
904
949
|
)
|
|
905
950
|
|
|
906
|
-
if self.debug
|
|
907
|
-
print(f"\n[OpenResponses] Response created: id={response.id}")
|
|
908
|
-
print(f"[OpenResponses] Response status: {response.status.value}")
|
|
951
|
+
if self.debug:
|
|
952
|
+
print(f"\n[OpenResponses stream] Response created: id={response.id}")
|
|
909
953
|
|
|
910
954
|
# Add user prompt to memory
|
|
911
955
|
self.memory.add("user", prompt)
|
|
@@ -914,22 +958,195 @@ Final Answer: <the answer>
|
|
|
914
958
|
user_item = create_message_item("user", prompt)
|
|
915
959
|
response.input.append(user_item)
|
|
916
960
|
|
|
917
|
-
if self.debug and not self._is_comp_mode:
|
|
918
|
-
print(f"[OpenResponses] Input item added: id={user_item.id}, type={user_item.type}, role={user_item.role}")
|
|
919
|
-
|
|
920
961
|
if self.debug:
|
|
921
|
-
print(f"\n[AgentNova] Model: {self.model}")
|
|
922
|
-
print(f"[AgentNova] Backend: {self.backend.base_url}")
|
|
923
|
-
print(f"[AgentNova] tool_choice: {self.tool_choice.type.value}")
|
|
924
|
-
print(f"[AgentNova] Tools: {self.tools.names()}")
|
|
925
|
-
print(f"[AgentNova] Prompt
|
|
962
|
+
print(f"\n[AgentNova stream] Model: {self.model}")
|
|
963
|
+
print(f"[AgentNova stream] Backend: {self.backend.base_url}")
|
|
964
|
+
print(f"[AgentNova stream] tool_choice: {self.tool_choice.type.value}")
|
|
965
|
+
print(f"[AgentNova stream] Tools: {self.tools.names()}")
|
|
966
|
+
print(f"[AgentNova stream] Prompt: {prompt}\n")
|
|
967
|
+
|
|
968
|
+
# OpenResponses: Agentic Loop (streaming variant)
|
|
969
|
+
# Stream model output, check for tool calls, execute them, repeat.
|
|
970
|
+
_expecting_final_answer = False
|
|
971
|
+
_last_successful_result = None
|
|
972
|
+
_last_tool_name = None
|
|
973
|
+
tool_call_count = 0
|
|
974
|
+
|
|
975
|
+
for step_num in range(self.max_steps):
|
|
976
|
+
if self.debug:
|
|
977
|
+
print(f"[Stream Step {step_num + 1}]")
|
|
978
|
+
|
|
979
|
+
# Collect the full streamed response
|
|
980
|
+
full_content = ""
|
|
981
|
+
|
|
982
|
+
# Stream model response, collecting content for tool-call detection
|
|
983
|
+
try:
|
|
984
|
+
for chunk in self._generate_stream_chunks(prompt):
|
|
985
|
+
full_content += chunk
|
|
986
|
+
except Exception as e:
|
|
987
|
+
if self.debug:
|
|
988
|
+
print(f" [Stream] ERROR: {e}")
|
|
989
|
+
# Emit failure event
|
|
990
|
+
response.mark_failed({"message": str(e), "type": "stream_error"})
|
|
991
|
+
fail_event = ResponseEvent(
|
|
992
|
+
type=EventType.RESPONSE_FAILED,
|
|
993
|
+
response=response,
|
|
994
|
+
)
|
|
995
|
+
yield fail_event.to_sse()
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
# Parse for tool calls (ReAct format)
|
|
999
|
+
tool_calls_found = []
|
|
1000
|
+
|
|
1001
|
+
if full_content:
|
|
1002
|
+
parsed_calls = self._parser.parse(full_content)
|
|
1003
|
+
for call in parsed_calls:
|
|
1004
|
+
if hasattr(call, 'thought') and call.thought:
|
|
1005
|
+
reasoning_item = ReasoningItem(
|
|
1006
|
+
content=[OutputText(text=call.thought)]
|
|
1007
|
+
)
|
|
1008
|
+
reasoning_item.status = ItemStatus.COMPLETED
|
|
1009
|
+
response.add_output_item(reasoning_item)
|
|
1010
|
+
|
|
1011
|
+
tool_calls_found.append({
|
|
1012
|
+
"name": call.name,
|
|
1013
|
+
"arguments": call.arguments,
|
|
1014
|
+
"id": "",
|
|
1015
|
+
"final_answer": getattr(call, 'final_answer', None),
|
|
1016
|
+
})
|
|
926
1017
|
|
|
927
|
-
|
|
928
|
-
|
|
1018
|
+
# ---- Execute tool calls if found ----
|
|
1019
|
+
if tool_calls_found:
|
|
1020
|
+
# Final Answer enforcement (same logic as run())
|
|
1021
|
+
if _expecting_final_answer and _last_successful_result is not None:
|
|
1022
|
+
text_chunks_gen = iter([_last_successful_result])
|
|
1023
|
+
for sse_event in stream_response_events(
|
|
1024
|
+
Response(model=self.model, status=ResponseStatus.IN_PROGRESS,
|
|
1025
|
+
tool_choice=self.tool_choice, allowed_tools=self._allowed_tools or []),
|
|
1026
|
+
text_chunks_gen, debug=self.debug,
|
|
1027
|
+
):
|
|
1028
|
+
yield sse_event
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
pending_final_answer = None
|
|
1032
|
+
self.memory.add("assistant", full_content)
|
|
1033
|
+
|
|
1034
|
+
for tc in tool_calls_found:
|
|
1035
|
+
tool_name = tc["name"]
|
|
1036
|
+
tool_args = tc["arguments"]
|
|
1037
|
+
|
|
1038
|
+
if tc.get("final_answer"):
|
|
1039
|
+
pending_final_answer = tc["final_answer"]
|
|
1040
|
+
|
|
1041
|
+
# Check allowed_tools
|
|
1042
|
+
if self._allowed_tools and tool_name not in self._allowed_tools:
|
|
1043
|
+
error_msg = f"Tool '{tool_name}' not in allowed_tools: {self._allowed_tools}"
|
|
1044
|
+
self.memory.add("user", f"Observation: Error: {error_msg}")
|
|
1045
|
+
continue
|
|
1046
|
+
|
|
1047
|
+
# Create FunctionCallItem and emit SSE events
|
|
1048
|
+
fc_item = create_function_call_item(tool_name, tool_args)
|
|
1049
|
+
fc_item.status = ItemStatus.IN_PROGRESS
|
|
1050
|
+
response.add_output_item(fc_item)
|
|
1051
|
+
output_index = len(response.output) - 1
|
|
1052
|
+
|
|
1053
|
+
fc_added = OutputItemEvent(
|
|
1054
|
+
type=EventType.OUTPUT_ITEM_ADDED,
|
|
1055
|
+
item=fc_item,
|
|
1056
|
+
output_index=output_index,
|
|
1057
|
+
)
|
|
1058
|
+
yield fc_added.to_sse()
|
|
1059
|
+
|
|
1060
|
+
# Execute the tool
|
|
1061
|
+
result = self._execute_tool(tool_name, tool_args, prompt)
|
|
1062
|
+
tool_call_count += 1
|
|
1063
|
+
|
|
1064
|
+
fc_item.status = ItemStatus.COMPLETED
|
|
1065
|
+
|
|
1066
|
+
fc_done = OutputItemEvent(
|
|
1067
|
+
type=EventType.OUTPUT_ITEM_DONE,
|
|
1068
|
+
item=fc_item,
|
|
1069
|
+
output_index=output_index,
|
|
1070
|
+
)
|
|
1071
|
+
yield fc_done.to_sse()
|
|
1072
|
+
|
|
1073
|
+
# Create function_call_output
|
|
1074
|
+
fco_item = create_function_call_output(fc_item.call_id, str(result))
|
|
1075
|
+
response.add_output_item(fco_item)
|
|
1076
|
+
|
|
1077
|
+
# Build observation and add to memory
|
|
1078
|
+
is_error = is_error_result(str(result))
|
|
1079
|
+
observation_msg = build_enhanced_observation(
|
|
1080
|
+
tool_name=tool_name,
|
|
1081
|
+
result=str(result),
|
|
1082
|
+
tracker=self._error_tracker,
|
|
1083
|
+
available_tools=self.tools.names(),
|
|
1084
|
+
is_error=is_error,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
if is_error:
|
|
1088
|
+
_expecting_final_answer = False
|
|
1089
|
+
_last_tool_name = None
|
|
1090
|
+
else:
|
|
1091
|
+
from .core.error_recovery import _is_simple_result
|
|
1092
|
+
if _is_simple_result(str(result), tool_name):
|
|
1093
|
+
_expecting_final_answer = True
|
|
1094
|
+
_last_successful_result = str(result)
|
|
1095
|
+
_last_tool_name = tool_name
|
|
1096
|
+
else:
|
|
1097
|
+
_expecting_final_answer = False
|
|
1098
|
+
_last_tool_name = None
|
|
1099
|
+
|
|
1100
|
+
self.memory.add("user", observation_msg)
|
|
1101
|
+
|
|
1102
|
+
# Check for pending final answer
|
|
1103
|
+
if pending_final_answer:
|
|
1104
|
+
text_chunks_gen = iter([pending_final_answer])
|
|
1105
|
+
for sse_event in stream_response_events(
|
|
1106
|
+
Response(model=self.model, status=ResponseStatus.IN_PROGRESS,
|
|
1107
|
+
tool_choice=self.tool_choice, allowed_tools=self._allowed_tools or []),
|
|
1108
|
+
text_chunks_gen, debug=self.debug,
|
|
1109
|
+
):
|
|
1110
|
+
yield sse_event
|
|
1111
|
+
return
|
|
1112
|
+
|
|
1113
|
+
# Continue the agentic loop (next streaming iteration)
|
|
1114
|
+
continue
|
|
1115
|
+
|
|
1116
|
+
# ---- No tool calls — stream final response ----
|
|
1117
|
+
# Check for Final Answer format
|
|
1118
|
+
if self._parser.is_final_answer(full_content):
|
|
1119
|
+
answer = self._parser.extract_final_answer(full_content)
|
|
1120
|
+
text_chunks_gen = iter([answer])
|
|
1121
|
+
else:
|
|
1122
|
+
text_chunks_gen = iter([full_content])
|
|
1123
|
+
|
|
1124
|
+
# Stream the final response with proper OpenResponses events
|
|
1125
|
+
final_response = Response(
|
|
1126
|
+
model=self.model,
|
|
1127
|
+
status=ResponseStatus.IN_PROGRESS,
|
|
1128
|
+
tool_choice=self.tool_choice,
|
|
1129
|
+
allowed_tools=self._allowed_tools or [],
|
|
1130
|
+
)
|
|
1131
|
+
# Carry over any items from previous loop iterations
|
|
1132
|
+
final_response.output = response.output
|
|
1133
|
+
final_response.input = response.input
|
|
1134
|
+
final_response.usage = response.usage
|
|
929
1135
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1136
|
+
for sse_event in stream_response_events(final_response, text_chunks_gen, debug=self.debug):
|
|
1137
|
+
yield sse_event
|
|
1138
|
+
|
|
1139
|
+
# Only one pass needed when there are no tool calls
|
|
1140
|
+
return
|
|
1141
|
+
|
|
1142
|
+
else:
|
|
1143
|
+
# Max steps reached
|
|
1144
|
+
response.mark_incomplete()
|
|
1145
|
+
incomplete_event = ResponseEvent(
|
|
1146
|
+
type=EventType.RESPONSE_INCOMPLETE,
|
|
1147
|
+
response=response,
|
|
1148
|
+
)
|
|
1149
|
+
yield incomplete_event.to_sse()
|
|
933
1150
|
|
|
934
1151
|
def _generate_stream_chunks(self, prompt: str) -> Generator[str, None, None]:
|
|
935
1152
|
"""
|
|
@@ -1076,6 +1293,11 @@ Final Answer: <the answer>
|
|
|
1076
1293
|
if self._num_predict is not None:
|
|
1077
1294
|
backend_kwargs["num_predict"] = self._num_predict
|
|
1078
1295
|
|
|
1296
|
+
# OpenResponses: Forward tool_choice to backend API
|
|
1297
|
+
# This allows the backend to enforce tool invocation constraints natively
|
|
1298
|
+
if self.tool_choice and self.tool_choice.type != ToolChoiceType.AUTO:
|
|
1299
|
+
backend_kwargs["tool_choice"] = self.tool_choice.to_dict()
|
|
1300
|
+
|
|
1079
1301
|
# Pass tools for native tool calling (OpenResponses/ChatCompletions compliant)
|
|
1080
1302
|
# ReAct parsing remains as fallback for models without native support
|
|
1081
1303
|
tools_for_backend = self.tools.all() if self.tools and len(self.tools) > 0 else None
|
|
@@ -1101,6 +1323,17 @@ Final Answer: <the answer>
|
|
|
1101
1323
|
**backend_kwargs,
|
|
1102
1324
|
)
|
|
1103
1325
|
|
|
1326
|
+
# OpenResponses / Chat Completions: Handle finish_reason
|
|
1327
|
+
# Per spec, finish_reason affects response status:
|
|
1328
|
+
# "stop" → normal completion (default)
|
|
1329
|
+
# "length" → incomplete — token budget exhausted
|
|
1330
|
+
# "content_filter" → failed — content was filtered
|
|
1331
|
+
finish_reason = response.get("finish_reason", "stop")
|
|
1332
|
+
if self.debug:
|
|
1333
|
+
print(f" [DEBUG] finish_reason: {finish_reason}")
|
|
1334
|
+
# Store for caller to consume
|
|
1335
|
+
response["_finish_reason"] = finish_reason
|
|
1336
|
+
|
|
1104
1337
|
if self.debug:
|
|
1105
1338
|
print(f" [DEBUG] Response keys: {list(response.keys())}")
|
|
1106
1339
|
print(f" [DEBUG] Content: {response.get('content', '')[:100]}...")
|
|
@@ -1115,12 +1348,7 @@ Final Answer: <the answer>
|
|
|
1115
1348
|
OpenResponses: Tool execution is straightforward - no synthesis.
|
|
1116
1349
|
Arguments must come from the model.
|
|
1117
1350
|
"""
|
|
1118
|
-
|
|
1119
|
-
matched_name = _fuzzy_match_tool_name(name, self.tools.names())
|
|
1120
|
-
if matched_name:
|
|
1121
|
-
name = matched_name
|
|
1122
|
-
|
|
1123
|
-
tool = self.tools.get_fuzzy(name)
|
|
1351
|
+
tool = self.tools.get(name)
|
|
1124
1352
|
|
|
1125
1353
|
if tool is None:
|
|
1126
1354
|
return f"Error: Unknown tool '{name}'. Available tools: {self.tools.names()}"
|