@octocodeai/freellama-mcp-server 0.5.0 → 0.6.0
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.
- package/CONFIGURATION.md +108 -0
- package/DEVELOPING.md +109 -0
- package/README.md +130 -282
- package/dist/index.js +1509 -1411
- package/dist/research/bash-agent.js +2435 -0
- package/dist/research/octocode-agent.js +1639 -0
- package/docs/ARCHITECTURE.md +145 -81
- package/docs/CLI.md +83 -55
- package/docs/CPU_GPU_ROUTING.md +4 -3
- package/docs/MODEL_SELECTION.md +115 -5
- package/docs/MONITORING.md +19 -11
- package/docs/PRODUCTION.md +10 -13
- package/docs/README.md +9 -13
- package/docs/SCOPES_AND_WARMING.md +8 -9
- package/docs/TESTING.md +18 -30
- package/native/index.d.ts +179 -174
- package/native/index.js +3 -3
- package/package.json +11 -10
- package/adapters/agent_context.py +0 -850
- package/adapters/agent_transport.py +0 -215
- package/adapters/bash_agent.py +0 -345
- package/adapters/octocode_agent.py +0 -404
- package/adapters/shell_sandbox.py +0 -487
- package/docs/ECONOMICS.md +0 -110
- package/docs/INDEX.md +0 -21
- package/docs/MODEL_METADATA.md +0 -106
- package/docs/OLLAMA_SIDECAR.md +0 -78
- package/docs/PRODUCT_POSITIONING.md +0 -121
- package/docs/dev/ADR_RESOURCE_AWARE_BACKEND_ROUTING.md +0 -55
- package/docs/dev/FINDINGS_AND_POSITIONING.md +0 -290
- package/docs/dev/LOCAL_PERFORMANCE_TESTING.md +0 -298
- package/docs/dev/OLLAMA_SYSTEM_OPTIMIZATION.md +0 -255
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Agent A adapter: local Ollama model whose only research tool is the `octocode` CLI.
|
|
3
|
-
|
|
4
|
-
Implements the FreeLlama benchmark adapter contract (benchmark/harness/references/adapters.md):
|
|
5
|
-
reads FREELLAMA_BENCH_MODEL/PROMPT/WORKSPACE/AGENT_RESULT, drives its own Ollama chat loop, and
|
|
6
|
-
writes agent-result.json. See benchmark/local/docs/02-agent-a-octocode.md for the full spec this
|
|
7
|
-
mirrors verbatim.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from contextlib import ExitStack
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
import subprocess
|
|
16
|
-
import time
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
19
|
-
from urllib.error import HTTPError, URLError
|
|
20
|
-
from urllib.request import Request, urlopen
|
|
21
|
-
|
|
22
|
-
from agent_context import (
|
|
23
|
-
AgentContextManager,
|
|
24
|
-
AgentRuntimeConfig,
|
|
25
|
-
PARSE_REPAIR_NOTICE,
|
|
26
|
-
REPEAT_NOTICE,
|
|
27
|
-
ObservationStore,
|
|
28
|
-
call_signature,
|
|
29
|
-
parse_json_action,
|
|
30
|
-
write_failure_result,
|
|
31
|
-
)
|
|
32
|
-
from agent_transport import chat_request, request_headers, unwrap_chat_response, PromptCacheUsage, retryable_chat_error, resolve_model_identity, chat_error_details
|
|
33
|
-
|
|
34
|
-
OCTOCODE_TOOLS = {"localViewStructure", "localFindFiles", "localSearchCode", "localGetFileContent", "lspGetSemantics"}
|
|
35
|
-
PATH_KEYS = ("path", "uri")
|
|
36
|
-
# Nothing is clipped any more — see the pagination section of agent_context.py. `calls[].result`
|
|
37
|
-
# goes to result.json on disk and never into the model's context, so it is stored in full; the model
|
|
38
|
-
# is shown one page at a time with an exact instruction for fetching the next.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def request_json(url: str, payload: dict[str, Any], timeout_seconds: float) -> dict[str, Any]:
|
|
42
|
-
request = Request(url, data=json.dumps(payload).encode(), headers=request_headers(), method="POST")
|
|
43
|
-
with urlopen(request, timeout=timeout_seconds) as response:
|
|
44
|
-
return json.loads(response.read())
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def safe_resolve(root: Path, value: str) -> Path:
|
|
48
|
-
candidate = Path(value)
|
|
49
|
-
candidate = candidate if candidate.is_absolute() else (root / candidate)
|
|
50
|
-
candidate = candidate.resolve()
|
|
51
|
-
if candidate != root and root not in candidate.parents:
|
|
52
|
-
raise ValueError(f"path escapes benchmark workspace: {value}")
|
|
53
|
-
if ".git" in candidate.parts:
|
|
54
|
-
raise ValueError("git metadata is outside the task surface")
|
|
55
|
-
return candidate
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def parse_action(content: str) -> dict[str, Any]:
|
|
59
|
-
value = parse_json_action(content)
|
|
60
|
-
action = value["action"]
|
|
61
|
-
if action == "octocode":
|
|
62
|
-
if value.get("tool") not in OCTOCODE_TOOLS:
|
|
63
|
-
raise ValueError(f"octocode action requires one supported tool: {sorted(OCTOCODE_TOOLS)}")
|
|
64
|
-
if not isinstance(value.get("queries"), dict):
|
|
65
|
-
raise ValueError("octocode action requires a queries object")
|
|
66
|
-
if action == "page" and (
|
|
67
|
-
not isinstance(value.get("step"), int)
|
|
68
|
-
or value["step"] < 1
|
|
69
|
-
or not isinstance(value.get("page"), int)
|
|
70
|
-
or value["page"] < 1
|
|
71
|
-
):
|
|
72
|
-
raise ValueError("page action requires positive integer step and page")
|
|
73
|
-
if action == "finish" and not isinstance(value.get("answer"), str):
|
|
74
|
-
raise ValueError("finish action requires a string answer")
|
|
75
|
-
if action not in {"octocode", "page", "finish"}:
|
|
76
|
-
raise ValueError(f"unsupported action for octocode adapter: {action}")
|
|
77
|
-
return value
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Pinned: an unpinned `npx octocode` downloads whatever is latest at call time (a supply-chain
|
|
81
|
-
# risk for a tool that reads the user's workspace) and the first download can exceed the tool
|
|
82
|
-
# timeout. Override deliberately with FREELLAMA_AGENT_OCTOCODE_PACKAGE.
|
|
83
|
-
OCTOCODE_PACKAGE = os.environ.get("FREELLAMA_AGENT_OCTOCODE_PACKAGE", "octocode@19.1.0")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def run_octocode(
|
|
87
|
-
root: Path,
|
|
88
|
-
tool_name: str,
|
|
89
|
-
queries: dict[str, Any],
|
|
90
|
-
timeout_seconds: float,
|
|
91
|
-
) -> str:
|
|
92
|
-
if tool_name not in OCTOCODE_TOOLS:
|
|
93
|
-
raise ValueError(f"unsupported octocode tool: {tool_name}")
|
|
94
|
-
resolved = dict(queries)
|
|
95
|
-
for key in PATH_KEYS:
|
|
96
|
-
if key in resolved and isinstance(resolved[key], str):
|
|
97
|
-
resolved[key] = str(safe_resolve(root, resolved[key]))
|
|
98
|
-
command = ["npx", "--yes", OCTOCODE_PACKAGE, "tools", tool_name, "--queries", json.dumps(resolved), "--compact"]
|
|
99
|
-
result = subprocess.run(
|
|
100
|
-
command,
|
|
101
|
-
cwd=root,
|
|
102
|
-
text=True,
|
|
103
|
-
capture_output=True,
|
|
104
|
-
timeout=timeout_seconds,
|
|
105
|
-
check=False,
|
|
106
|
-
)
|
|
107
|
-
output = (result.stdout or "") + (("\nSTDERR:\n" + result.stderr) if result.returncode != 0 else "")
|
|
108
|
-
if not output.strip():
|
|
109
|
-
output = "(empty result)"
|
|
110
|
-
return output
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def system_prompt(workspace: str) -> str:
|
|
114
|
-
return f"""You are a local code-research agent in a read-only workspace at {workspace}. Read or search files only through the `octocode` local tools below. Return exactly one JSON object per turn.
|
|
115
|
-
|
|
116
|
-
Tools (call as {{"action":"octocode","tool":"<name>","queries":{{...}}}}):
|
|
117
|
-
|
|
118
|
-
localViewStructure - browse a directory tree, no content loaded; cheapest first orientation step.
|
|
119
|
-
path (string, required, absolute), maxDepth (int), recursive (bool), filesOnly (bool),
|
|
120
|
-
directoriesOnly (bool), pattern (glob/substring filter).
|
|
121
|
-
|
|
122
|
-
localFindFiles - find files/dirs by name, glob, regex, or type; returns paths only, not content.
|
|
123
|
-
path (string, required, absolute), names (array of globs), pathPattern (glob over full path),
|
|
124
|
-
regex (basename regex), entryType ("f"|"d"), excludeDir (array, e.g. ["node_modules",".git"]).
|
|
125
|
-
|
|
126
|
-
localSearchCode - search file contents for text/regex; returns file+line matches. Your main tool.
|
|
127
|
-
path (string, required, absolute), keywords (string; literal or regex search term),
|
|
128
|
-
mode ("paginated"|"discovery"|"detailed"), include/exclude (glob arrays),
|
|
129
|
-
caseInsensitive (bool), maxFiles (int).
|
|
130
|
-
Do NOT pass mode "structural" with keywords — structural (AST) search takes `pattern`/`rule`
|
|
131
|
-
instead and hard-errors on keywords, costing you a turn for nothing.
|
|
132
|
-
|
|
133
|
-
localGetFileContent - read one file or a line range/matched slice of it.
|
|
134
|
-
path (string, required, absolute), fullContent (bool; small files only), startLine +
|
|
135
|
-
endLine (ints, both required together), matchString (anchor text/regex), minify
|
|
136
|
-
("none"|"standard"|"symbols" — "symbols" gives a cheap outline first).
|
|
137
|
-
|
|
138
|
-
lspGetSemantics - LSP semantic queries: definitions, references, callers/callees, symbol outline.
|
|
139
|
-
uri (string, required, absolute path), type ("definition"|"references"|"callers"|
|
|
140
|
-
"callees"|"documentSymbols"|"hover"), symbolName (exact name), lineHint (int from a prior
|
|
141
|
-
search result — never guess it).
|
|
142
|
-
An empty result is ambiguous: indexing or unavailable analysis can produce no symbols.
|
|
143
|
-
Retry once or use localSearchCode; an empty LSP result alone does not prove absence.
|
|
144
|
-
|
|
145
|
-
Read another page of an earlier step: {{"action":"page","step":2,"page":2}}
|
|
146
|
-
Full tool output is stored in pages; you see page 1 and the total. Context fitting can shorten
|
|
147
|
-
an observation in the conversation. Request its stored page to recover the evidence without
|
|
148
|
-
re-running the tool. Claim "not found" only after checking the relevant search scope and pages.
|
|
149
|
-
|
|
150
|
-
Finish with: {{"action":"finish","answer":"concise final answer with repository-relative evidence"}}
|
|
151
|
-
|
|
152
|
-
These are the complete action schemas. Use only `octocode`, `page`, or `finish`; every shown field
|
|
153
|
-
is required with the shown type, and `tool` must be one of the five names above. An invalid shape
|
|
154
|
-
is rejected and costs one bounded repair turn.
|
|
155
|
-
|
|
156
|
-
SCOPE YOUR SEARCHES. Pass excludeDir/exclude on every search in a real workspace —
|
|
157
|
-
["node_modules","target",".venv","dist","build",".git","vendor","__pycache__",".octocode"] — or
|
|
158
|
-
vendored and generated files will bury the answer. For implementation questions, prefer src/,
|
|
159
|
-
packages/*/src/ and lib/ before fixtures, mocks, or examples. If the question targets tests or examples,
|
|
160
|
-
inspect those files. Name the file that supplies the answer. Absence of a match is weak evidence: widen the pattern before concluding
|
|
161
|
-
something does not exist.
|
|
162
|
-
For a production default, find its declaration before relying on test occurrences. Declarations look like
|
|
163
|
-
`default=`, `unwrap_or(`, `const `, `static `, or a settings schema initializer.
|
|
164
|
-
For a test-specific default or fixture, inspect its test declaration.
|
|
165
|
-
|
|
166
|
-
All paths you pass must resolve inside the workspace; relative paths are resolved against the
|
|
167
|
-
workspace root automatically. Orient cheap (localViewStructure) before reading in
|
|
168
|
-
full. Be decisive: most tasks need 2-6 tool calls. Never edit or write files — you only have
|
|
169
|
-
read-only tools. Call finish as soon as the requested facts are established."""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _main(resources: ExitStack) -> int:
|
|
173
|
-
model = os.environ.get("FREELLAMA_TARGET_MODEL") or os.environ["FREELLAMA_BENCH_MODEL"]
|
|
174
|
-
workspace = Path(os.environ["FREELLAMA_BENCH_WORKSPACE"]).resolve()
|
|
175
|
-
prompt = Path(os.environ["FREELLAMA_BENCH_PROMPT"]).read_text(encoding="utf-8")
|
|
176
|
-
result_path = Path(os.environ["FREELLAMA_AGENT_RESULT"])
|
|
177
|
-
endpoint = os.environ.get("FREELLAMA_OLLAMA_ENDPOINT", "http://127.0.0.1:11434").rstrip("/")
|
|
178
|
-
managed_endpoint = os.environ.get("FREELLAMA_AGENT_MANAGED_ENDPOINT", "").rstrip("/")
|
|
179
|
-
execution_preference = os.environ.get("FREELLAMA_AGENT_EXECUTION_PREFERENCE", "auto")
|
|
180
|
-
min_placement_evidence = os.environ.get("FREELLAMA_AGENT_MIN_PLACEMENT_EVIDENCE", "configured")
|
|
181
|
-
try:
|
|
182
|
-
runtime = AgentRuntimeConfig.from_env(default_tool_timeout_seconds=45)
|
|
183
|
-
except ValueError as error:
|
|
184
|
-
answer = f"invalid research adapter configuration: {error}"
|
|
185
|
-
write_failure_result(result_path, answer)
|
|
186
|
-
print(answer)
|
|
187
|
-
return 1
|
|
188
|
-
messages: list[dict[str, Any]] = [
|
|
189
|
-
{"role": "system", "content": system_prompt(str(workspace))},
|
|
190
|
-
{"role": "user", "content": prompt},
|
|
191
|
-
]
|
|
192
|
-
calls: list[dict[str, Any]] = []
|
|
193
|
-
usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_tokens": None, "cache_write_tokens": None}
|
|
194
|
-
cache_usage = PromptCacheUsage()
|
|
195
|
-
metrics = {"load_ms": 0.0, "prompt_eval_ms": 0.0, "eval_ms": 0.0}
|
|
196
|
-
execution_receipts: list[dict[str, Any]] = []
|
|
197
|
-
answer = ""
|
|
198
|
-
failure: str | None = None
|
|
199
|
-
transport_error: dict[str, Any] | None = None
|
|
200
|
-
calibration_dir = os.environ.get("FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR", "").strip()
|
|
201
|
-
calibration_model_identity = (
|
|
202
|
-
resolve_model_identity(managed_endpoint or endpoint, model, messages[0]["content"])
|
|
203
|
-
if calibration_dir else {"verified": False, "scope": "persistence_disabled", "identity": ""}
|
|
204
|
-
)
|
|
205
|
-
context_manager = AgentContextManager(
|
|
206
|
-
runtime,
|
|
207
|
-
model=model,
|
|
208
|
-
calibration_identity=calibration_model_identity["identity"],
|
|
209
|
-
calibration_dir=Path(calibration_dir) if calibration_dir and calibration_model_identity["verified"] else None,
|
|
210
|
-
)
|
|
211
|
-
try:
|
|
212
|
-
messages = context_manager.fit(messages)
|
|
213
|
-
except ValueError as error:
|
|
214
|
-
answer = f"research task does not fit the configured context safely: {error}"
|
|
215
|
-
write_failure_result(result_path, answer)
|
|
216
|
-
print(answer)
|
|
217
|
-
return 1
|
|
218
|
-
chat_options = {
|
|
219
|
-
"temperature": runtime.temperature,
|
|
220
|
-
"seed": runtime.seed,
|
|
221
|
-
"num_ctx": runtime.num_ctx,
|
|
222
|
-
"num_predict": runtime.num_predict,
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
def call_model() -> dict[str, Any]:
|
|
226
|
-
# Retry explicit overload/connection refusals only. Replaying a timeout or an uncertain
|
|
227
|
-
# transport failure can overlap a generation that is still consuming the runner.
|
|
228
|
-
last_error: Exception | None = None
|
|
229
|
-
for attempt in range(runtime.retry_attempts):
|
|
230
|
-
try:
|
|
231
|
-
transport_endpoint = (
|
|
232
|
-
f"{managed_endpoint}/_freellama/v1/tasks" if managed_endpoint else endpoint
|
|
233
|
-
)
|
|
234
|
-
url, payload = chat_request(
|
|
235
|
-
transport_endpoint,
|
|
236
|
-
model,
|
|
237
|
-
messages,
|
|
238
|
-
chat_options,
|
|
239
|
-
runtime.think,
|
|
240
|
-
runtime.keep_alive,
|
|
241
|
-
execution_preference,
|
|
242
|
-
min_placement_evidence,
|
|
243
|
-
)
|
|
244
|
-
response = unwrap_chat_response(
|
|
245
|
-
request_json(url, payload, runtime.request_timeout_seconds), execution_receipts
|
|
246
|
-
)
|
|
247
|
-
break
|
|
248
|
-
except (HTTPError, URLError, TimeoutError) as error:
|
|
249
|
-
if not retryable_chat_error(error):
|
|
250
|
-
raise
|
|
251
|
-
last_error = error
|
|
252
|
-
if attempt + 1 < runtime.retry_attempts:
|
|
253
|
-
time.sleep(runtime.retry_backoff_seconds)
|
|
254
|
-
else:
|
|
255
|
-
raise last_error # type: ignore[misc]
|
|
256
|
-
usage["input_tokens"] += int(response.get("prompt_eval_count", 0))
|
|
257
|
-
cache_usage.observe(response)
|
|
258
|
-
usage["cache_read_tokens"] = cache_usage.tokens
|
|
259
|
-
usage["output_tokens"] += int(response.get("eval_count", 0))
|
|
260
|
-
metrics["load_ms"] += float(response.get("load_duration", 0)) / 1_000_000
|
|
261
|
-
metrics["prompt_eval_ms"] += float(response.get("prompt_eval_duration", 0)) / 1_000_000
|
|
262
|
-
metrics["eval_ms"] += float(response.get("eval_duration", 0)) / 1_000_000
|
|
263
|
-
context_manager.observe(messages, response.get("prompt_eval_count"))
|
|
264
|
-
return response
|
|
265
|
-
|
|
266
|
-
def refit() -> bool:
|
|
267
|
-
nonlocal messages, failure
|
|
268
|
-
try:
|
|
269
|
-
messages = context_manager.fit(messages)
|
|
270
|
-
return True
|
|
271
|
-
except ValueError as error:
|
|
272
|
-
failure = f"research context cannot be fitted safely: {error}"
|
|
273
|
-
return False
|
|
274
|
-
|
|
275
|
-
seen_calls: dict[str, int] = {}
|
|
276
|
-
observations = resources.enter_context(ObservationStore(runtime.context.observation_page_chars))
|
|
277
|
-
parse_failures = 0
|
|
278
|
-
for _ in range(runtime.max_turns):
|
|
279
|
-
# Transport failures are terminal (call_model already retried them). A *parse* failure is
|
|
280
|
-
# not: tell the model what was wrong with its reply and let it correct itself, keeping every
|
|
281
|
-
# tool result gathered so far.
|
|
282
|
-
try:
|
|
283
|
-
response = call_model()
|
|
284
|
-
except (HTTPError, URLError, TimeoutError) as error:
|
|
285
|
-
diagnostic, transport_error = chat_error_details(error)
|
|
286
|
-
failure = f"agent response failed: {diagnostic}"
|
|
287
|
-
break
|
|
288
|
-
raw = str(response.get("message", {}).get("content", ""))
|
|
289
|
-
try:
|
|
290
|
-
action = parse_action(raw)
|
|
291
|
-
except (ValueError, json.JSONDecodeError) as error:
|
|
292
|
-
parse_failures += 1
|
|
293
|
-
if parse_failures > runtime.max_parse_repairs:
|
|
294
|
-
failure = f"agent gave {parse_failures} unparseable replies: {type(error).__name__}: {error}"
|
|
295
|
-
break
|
|
296
|
-
messages.append({"role": "assistant", "content": raw[: runtime.parse_repair_echo_chars]})
|
|
297
|
-
messages.append({"role": "user", "content": PARSE_REPAIR_NOTICE})
|
|
298
|
-
if not refit():
|
|
299
|
-
break
|
|
300
|
-
continue
|
|
301
|
-
parse_failures = 0
|
|
302
|
-
# Paging re-serves stored output. It costs a turn but never a subprocess, and no byte of
|
|
303
|
-
# any observation is ever unreachable — which is the whole point of paginating instead of
|
|
304
|
-
# clipping.
|
|
305
|
-
if action["action"] == "page":
|
|
306
|
-
try:
|
|
307
|
-
want_step = int(action.get("step", 0))
|
|
308
|
-
want_page = int(action.get("page", 1))
|
|
309
|
-
except (TypeError, ValueError):
|
|
310
|
-
want_step, want_page = 0, 1
|
|
311
|
-
messages.append({"role": "assistant", "content": json.dumps(action, ensure_ascii=False)})
|
|
312
|
-
messages.append({"role": "user", "content": observations.message(want_step, want_page)})
|
|
313
|
-
if not refit():
|
|
314
|
-
break
|
|
315
|
-
continue
|
|
316
|
-
if action["action"] == "finish":
|
|
317
|
-
answer = str(action.get("answer", "")).strip()
|
|
318
|
-
break
|
|
319
|
-
started = time.perf_counter()
|
|
320
|
-
tool_name = str(action.get("tool", ""))
|
|
321
|
-
queries = action.get("queries", {}) if isinstance(action.get("queries"), dict) else {}
|
|
322
|
-
# An exact repeat costs a turn out of ten AND a second copy of an observation the model
|
|
323
|
-
# has already seen. Answer it from the prior step instead of re-running the subprocess.
|
|
324
|
-
signature = call_signature({"tool": tool_name, "queries": queries})
|
|
325
|
-
repeated = signature in seen_calls
|
|
326
|
-
if repeated:
|
|
327
|
-
observation = REPEAT_NOTICE
|
|
328
|
-
status = "repeat"
|
|
329
|
-
else:
|
|
330
|
-
try:
|
|
331
|
-
observation = run_octocode(
|
|
332
|
-
workspace, tool_name, queries, runtime.tool_timeout_seconds
|
|
333
|
-
)
|
|
334
|
-
status = "ok"
|
|
335
|
-
except (OSError, ValueError, subprocess.TimeoutExpired) as error:
|
|
336
|
-
observation = f"tool error: {type(error).__name__}: {error}"
|
|
337
|
-
status = "error"
|
|
338
|
-
seen_calls[signature] = len(calls) + 1
|
|
339
|
-
calls.append({
|
|
340
|
-
"name": f"mcp.octocode.{tool_name}" if tool_name else "mcp.octocode.unknown",
|
|
341
|
-
"raw_name": action.get("action", ""),
|
|
342
|
-
"arguments": {"tool": tool_name, "queries": queries},
|
|
343
|
-
"status": status,
|
|
344
|
-
"duration_ms": round((time.perf_counter() - started) * 1000, 3),
|
|
345
|
-
})
|
|
346
|
-
messages.append({"role": "assistant", "content": json.dumps(action, ensure_ascii=False)})
|
|
347
|
-
remaining = runtime.max_turns - len(calls)
|
|
348
|
-
step = len(calls)
|
|
349
|
-
observations.put(step, observation, metadata={"action": tool_name, "status": status, "source": json.dumps(queries, ensure_ascii=False)})
|
|
350
|
-
del observation
|
|
351
|
-
messages.append({"role": "user", "content": observations.message(step) + f"\n\nTool calls remaining: {remaining}. Finish now if the task is answerable; do not repeat prior calls."})
|
|
352
|
-
if not refit():
|
|
353
|
-
break
|
|
354
|
-
else:
|
|
355
|
-
messages.append({"role": "user", "content": "Tool budget is exhausted. Return exactly a finish JSON action now using the evidence collected. Do not request another tool."})
|
|
356
|
-
if refit():
|
|
357
|
-
try:
|
|
358
|
-
response = call_model()
|
|
359
|
-
final_action = parse_action(str(response.get("message", {}).get("content", "")))
|
|
360
|
-
if final_action.get("action") != "finish":
|
|
361
|
-
raise ValueError("forced final response was not finish")
|
|
362
|
-
answer = str(final_action.get("answer", "")).strip()
|
|
363
|
-
except (HTTPError, URLError, TimeoutError, ValueError, json.JSONDecodeError) as error:
|
|
364
|
-
diagnostic, transport_error = chat_error_details(error)
|
|
365
|
-
failure = f"agent exceeded {runtime.max_turns} turns and finalization failed: {diagnostic}"
|
|
366
|
-
|
|
367
|
-
if not answer:
|
|
368
|
-
answer = failure or "agent stopped without a final answer"
|
|
369
|
-
result = {
|
|
370
|
-
"final_answer": answer,
|
|
371
|
-
"tool_calls": calls,
|
|
372
|
-
"usage": usage,
|
|
373
|
-
"provider_metrics": metrics,
|
|
374
|
-
"model_metadata": {
|
|
375
|
-
"adapter": "octocode_cli_agent_v1",
|
|
376
|
-
"temperature": chat_options["temperature"],
|
|
377
|
-
"seed": chat_options["seed"],
|
|
378
|
-
"num_ctx": chat_options["num_ctx"],
|
|
379
|
-
"num_predict": chat_options["num_predict"],
|
|
380
|
-
"max_turns": runtime.max_turns,
|
|
381
|
-
"runtime_config": runtime.metadata(),
|
|
382
|
-
"context_management": context_manager.metadata(),
|
|
383
|
-
"context_compactions": context_manager.compactions,
|
|
384
|
-
"transport": "managed" if managed_endpoint else "direct_ollama",
|
|
385
|
-
"execution_preference": execution_preference,
|
|
386
|
-
"min_placement_evidence": min_placement_evidence,
|
|
387
|
-
"execution_receipts": execution_receipts,
|
|
388
|
-
**({"transport_error": transport_error} if transport_error else {}),
|
|
389
|
-
"cache_token_metrics": cache_usage.metadata(),
|
|
390
|
-
"calibration_model_identity": calibration_model_identity,
|
|
391
|
-
},
|
|
392
|
-
}
|
|
393
|
-
observations.write_result(result_path, result)
|
|
394
|
-
print(answer)
|
|
395
|
-
return 1 if failure else 0
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
def main() -> int:
|
|
399
|
-
with ExitStack() as resources:
|
|
400
|
-
return _main(resources)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if __name__ == "__main__":
|
|
404
|
-
raise SystemExit(main())
|