@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.
@@ -1,215 +0,0 @@
1
- """Shared Ollama/FreeLlama chat transport for local research adapters."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import hashlib
7
- import json
8
- import re
9
- from pathlib import Path
10
- from typing import Any
11
- from urllib.error import HTTPError, URLError
12
- from urllib.request import Request, urlopen
13
-
14
-
15
- def _read_identity_json(url: str) -> dict[str, Any]:
16
- with urlopen(Request(url, headers=request_headers()), timeout=5) as response:
17
- payload = response.read(1024 * 1024 + 1)
18
- if len(payload) > 1024 * 1024:
19
- raise ValueError("model identity catalog exceeds size limit")
20
- return json.loads(payload)
21
-
22
-
23
- def resolve_model_identity(
24
- endpoint: str, model: str, system_prompt: str, *, read_json=None,
25
- ) -> dict[str, Any]:
26
- """Resolve immutable manifest identity once per adapter run, without loading a model.
27
-
28
- Managed catalogs honor exact CPU backend assignment. Failure disables persistent calibration;
29
- a mutable tag or a hash of the prompt alone must never masquerade as model identity.
30
- """
31
- endpoint = endpoint.rstrip("/")
32
- managed_suffix = "/_freellama/v1/tasks"
33
- url = (endpoint[:-len(managed_suffix)] + "/_freellama/v1/models"
34
- if endpoint.endswith(managed_suffix) else endpoint + "/api/tags")
35
- unknown = {"identity": "", "verified": False, "scope": "current_process_only"}
36
- try:
37
- payload = (read_json or _read_identity_json)(url)
38
- entries = payload.get("models", []) if isinstance(payload, dict) else []
39
- if not isinstance(entries, list):
40
- return unknown
41
- names = {model}
42
- if ":" not in model.rsplit("/", 1)[-1]:
43
- names.add(model + ":latest")
44
- entry = next((item for item in entries if isinstance(item, dict)
45
- and item.get("name", item.get("model")) in names), None)
46
- digest = entry.get("digest") if entry else None
47
- if not isinstance(digest, str) or not re.fullmatch(r"(?:sha256:)?[0-9a-fA-F]{64}", digest):
48
- return unknown
49
- digest = digest.removeprefix("sha256:").lower()
50
- contract = {"endpoint": endpoint, "model": model, "digest": digest,
51
- "system_hash": hashlib.sha256(system_prompt.encode("utf-8")).hexdigest()}
52
- return {"identity": hashlib.sha256(json.dumps(contract, sort_keys=True).encode()).hexdigest(),
53
- "verified": True, "scope": "manifest_digest_and_system_prompt", "digest": digest}
54
- except (OSError, ValueError, TypeError):
55
- return unknown
56
-
57
-
58
- def retryable_chat_error(error: Exception) -> bool:
59
- """Retry known refusals only; a timeout may leave inference running upstream."""
60
- if isinstance(error, HTTPError):
61
- return error.code == 503
62
- return isinstance(error, URLError) and isinstance(error.reason, ConnectionRefusedError)
63
-
64
-
65
- def chat_error_details(error: Exception) -> tuple[str, dict[str, Any] | None]:
66
- """Retain bounded application refusal receipts, excluding request/auth payloads."""
67
- diagnostic = f"{type(error).__name__}: {error}"
68
- if not isinstance(error, HTTPError):
69
- return diagnostic, None
70
- details: dict[str, Any] = {"status": error.code}
71
- try:
72
- body = error.read(64 * 1024 + 1)
73
- if len(body) > 64 * 1024:
74
- return diagnostic, details
75
- payload = json.loads(body)
76
- if not isinstance(payload, dict) or not isinstance(payload.get("error"), str):
77
- return diagnostic, details
78
- receipt_fields = {
79
- "error", "code", "reason", "retry_after_seconds", "resource_admission", "lifecycle",
80
- "placement", "execution", "admission", "route", "decision", "quality", "feedback",
81
- }
82
- payload_fields = {
83
- "prompt", "system", "system_prompt", "messages", "content", "input", "images", "tools",
84
- "request", "request_options", "headers", "authorization", "token", "api_key", "auth_token",
85
- }
86
-
87
- def receipt_value(value: Any) -> Any:
88
- if isinstance(value, dict):
89
- return {key: receipt_value(item) for key, item in value.items() if key.lower() not in payload_fields}
90
- if isinstance(value, list):
91
- return [receipt_value(item) for item in value]
92
- return value
93
-
94
- receipt = {key: receipt_value(value) for key, value in payload.items() if key in receipt_fields}
95
- details["receipt"] = receipt
96
- diagnostic = f"HTTP {error.code}: {receipt['error']}"
97
- except (OSError, ValueError, RecursionError):
98
- pass
99
- finally:
100
- error.close()
101
- return diagnostic, details
102
-
103
-
104
- class PromptCacheUsage:
105
- """Aggregate optional Ollama cache reads without treating unavailable turns as zero."""
106
-
107
- def __init__(self) -> None:
108
- self.calls = 0
109
- self.reported_calls = 0
110
- self.reported_tokens = 0
111
- self.reported_prompt_tokens = 0
112
-
113
- def observe(self, response: dict[str, Any]) -> None:
114
- self.calls += 1
115
- cached = response.get("prompt_eval_cached_count")
116
- prompt = response.get("prompt_eval_count")
117
- if type(cached) is int and type(prompt) is int and 0 <= cached <= prompt:
118
- self.reported_calls += 1
119
- self.reported_tokens += cached
120
- self.reported_prompt_tokens += prompt
121
-
122
- @property
123
- def tokens(self) -> int | None:
124
- return self.reported_tokens if self.calls > 0 and self.calls == self.reported_calls else None
125
-
126
- def metadata(self) -> dict[str, Any]:
127
- return {
128
- "status": "reported" if self.tokens is not None else (
129
- "partially_reported" if self.reported_calls else "not_reported"
130
- ),
131
- "calls": self.calls,
132
- "reported_calls": self.reported_calls,
133
- "reported_tokens": self.reported_tokens if self.reported_calls else None,
134
- "hit_ratio": self.reported_tokens / self.reported_prompt_tokens
135
- if self.tokens is not None and self.reported_prompt_tokens else None,
136
- }
137
-
138
-
139
- def request_headers() -> dict[str, str]:
140
- """Build transport headers, reading a bearer token from a file rather than process args."""
141
- headers = {"content-type": "application/json"}
142
- token_file = os.environ.get("FREELLAMA_AUTH_TOKEN_FILE", "").strip()
143
- if not token_file:
144
- return headers
145
- token = Path(token_file).read_text(encoding="utf-8").strip()
146
- if len(token) < 32 or any(character.isspace() for character in token):
147
- raise ValueError("FREELLAMA_AUTH_TOKEN_FILE must contain one token of at least 32 bytes")
148
- headers["authorization"] = f"Bearer {token}"
149
- return headers
150
-
151
-
152
- def chat_request(
153
- endpoint: str,
154
- model: str,
155
- messages: list[dict[str, Any]],
156
- options: dict[str, Any],
157
- think: bool,
158
- keep_alive: str,
159
- execution_preference: str = "auto",
160
- min_placement_evidence: str = "configured",
161
- ) -> tuple[str, dict[str, Any]]:
162
- """Build one direct-Ollama or managed-FreeLlama request.
163
-
164
- `endpoint` selects the transport: a URL ending in `/_freellama/v1/tasks` is managed. The
165
- benchmark can still target raw Ollama for controlled comparisons; MCP always supplies the
166
- managed URL so coding-agent turns share routing, admission, physical-placement receipts, and
167
- adaptive feedback with ordinary `run_task` calls.
168
- """
169
- endpoint = endpoint.rstrip("/")
170
- if endpoint.endswith("/_freellama/v1/tasks"):
171
- managed_options = {key: value for key, value in options.items() if key != "num_ctx"}
172
- return endpoint, {
173
- "task": "coding",
174
- "objective": "fastest",
175
- "model": model,
176
- "context_tokens": options["num_ctx"],
177
- "execution_preference": execution_preference,
178
- "min_placement_evidence": min_placement_evidence,
179
- "messages": messages,
180
- "keep_alive": keep_alive,
181
- "request_options": {
182
- "format": "json",
183
- "think": think,
184
- "options": managed_options,
185
- },
186
- }
187
- return f"{endpoint}/api/chat", {
188
- "model": model,
189
- "messages": messages,
190
- "stream": False,
191
- "truncate": False,
192
- "shift": False,
193
- "format": "json",
194
- "think": think,
195
- "keep_alive": keep_alive,
196
- "options": options,
197
- }
198
-
199
-
200
- def unwrap_chat_response(
201
- payload: dict[str, Any], execution_receipts: list[dict[str, Any]]
202
- ) -> dict[str, Any]:
203
- """Return the Ollama response and retain managed execution proof when present."""
204
- response = payload.get("response")
205
- if not isinstance(response, dict):
206
- return payload
207
- execution_receipts.append(
208
- {
209
- "model": payload.get("route", {}).get("selected_model"),
210
- "execution": payload.get("execution"),
211
- "admission": payload.get("admission"),
212
- "feedback": payload.get("feedback"),
213
- }
214
- )
215
- return response
@@ -1,345 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Agent B adapter: local Ollama model restricted to raw Linux/bash commands only.
3
-
4
- Same adapter contract and decoding settings as octocode_agent.py (see
5
- benchmark/harness/references/adapters.md and benchmark/local/docs/03-agent-b-bash.md) — the only
6
- variable under test is the tool surface: this agent has no structured tool schema, just a shell.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from contextlib import ExitStack
12
- import json
13
- import os
14
- import subprocess
15
- import time
16
- from pathlib import Path
17
- from typing import Any
18
- from urllib.error import HTTPError, URLError
19
- from urllib.request import Request, urlopen
20
-
21
- from agent_context import (
22
- AgentContextManager,
23
- AgentRuntimeConfig,
24
- PARSE_REPAIR_NOTICE,
25
- REPEAT_NOTICE,
26
- ObservationStore,
27
- call_signature,
28
- parse_json_action,
29
- write_failure_result,
30
- )
31
- import shell_sandbox
32
- from shell_sandbox import ReadOnlyShell
33
- from agent_transport import chat_request, request_headers, unwrap_chat_response, PromptCacheUsage, retryable_chat_error, resolve_model_identity, chat_error_details
34
-
35
- # Nothing is clipped any more. `calls[].result` is written to result.json on disk and read by the
36
- # MCP layer — it never enters the model's context, so there was never a reason to shorten it. What
37
- # the MODEL sees is paginated instead: one page plus an exact instruction for fetching the next.
38
- # See the pagination section of agent_context.py for why clipping was data loss, not economy.
39
-
40
- # Validation, restricted bash, a scrubbed environment and (where the host allows) an OS sandbox
41
- # live in shell_sandbox; see its module docstring for why the old regex denylist was not enough.
42
- assert_command_confined = shell_sandbox.assert_command_confined
43
-
44
-
45
- def request_json(url: str, payload: dict[str, Any], timeout_seconds: float) -> dict[str, Any]:
46
- request = Request(url, data=json.dumps(payload).encode(), headers=request_headers(), method="POST")
47
- with urlopen(request, timeout=timeout_seconds) as response:
48
- return json.loads(response.read())
49
-
50
-
51
- def parse_action(content: str) -> dict[str, Any]:
52
- value = parse_json_action(content)
53
- action = value["action"]
54
- if action == "shell" and (not isinstance(value.get("command"), str) or not value["command"].strip()):
55
- raise ValueError("shell action requires a non-empty string command")
56
- if action == "page" and (
57
- not isinstance(value.get("step"), int)
58
- or value["step"] < 1
59
- or not isinstance(value.get("page"), int)
60
- or value["page"] < 1
61
- ):
62
- raise ValueError("page action requires positive integer step and page")
63
- if action == "finish" and not isinstance(value.get("answer"), str):
64
- raise ValueError("finish action requires a string answer")
65
- if action not in {"shell", "page", "finish"}:
66
- raise ValueError(f"unsupported action for bash adapter: {action}")
67
- return value
68
-
69
-
70
- def run_shell(root: Path, command_text: str, timeout_seconds: float, shell: ReadOnlyShell | None = None) -> str:
71
- if shell is not None:
72
- return shell.run(command_text, timeout_seconds)
73
- with ReadOnlyShell(root) as scoped:
74
- return scoped.run(command_text, timeout_seconds)
75
-
76
-
77
- def system_prompt() -> str:
78
- return """You are a local code-research agent in a read-only workspace rooted at the current directory. Use raw POSIX shell commands without editors, special tools, or network access. Return exactly one JSON object per turn:
79
-
80
- {"action":"shell","command":"one shell command, e.g. grep -n \\"class Group\\" click/src/click/core.py"}
81
- {"action":"page","step":2,"page":2}
82
- {"action":"finish","answer":"concise final answer with repository-relative evidence"}
83
-
84
- These are the complete action schemas. Use only `shell`, `page`, or `finish`; every shown field is
85
- required with the shown type. An invalid shape is rejected and costs one bounded repair turn.
86
-
87
- Full command output is stored in pages; you see page 1 and the total. Context fitting can shorten
88
- an observation in the conversation. Request its stored page to recover the evidence without
89
- re-running the command. Claim "not found" only after checking the relevant search scope and pages.
90
-
91
- The shell is READ-ONLY and restricted. Available tools: ls, find, grep, rg, cat, head, tail, wc,
92
- sort, uniq, cut, tr, nl, sed (bounded read-only inline programs), jq, diff, xargs
93
- (only echo, printf, true, or false targets), tree, file, stat, and read-only git
94
- (log, show, grep, ls-files, diff, blame, status). There is no awk, python, network, output
95
- redirection, script-file loading, recursive symlink following, paths outside the workspace,
96
- $(...) substitution, $VARIABLES, or loops: write paths and patterns literally and
97
- chain tools with pipes, one pipeline per turn. A refused command costs a turn, so follow these
98
- rules. Be decisive: most tasks need 2-6 commands. Call finish as soon as the requested facts are
99
- established.
100
-
101
- SCOPE YOUR SEARCHES. A real workspace holds far more than its source, and an unscoped grep drowns
102
- the answer in vendored and generated files. Always exclude them:
103
- grep -rn "PATTERN" . --exclude-dir={node_modules,target,.venv,venv,dist,build,.git,vendor,__pycache__,.octocode,site-packages}
104
- For implementation questions, prefer src/, packages/*/src/, lib/ and the repo root before fixtures,
105
- mocks, or examples. If the question targets tests or examples, inspect those files. Name the file
106
- that supplies the answer.
107
- If a search returns nothing, widen the pattern before concluding the thing does not exist: absence
108
- of a grep hit is weak evidence, and "not found" is only a real answer once you have looked in the
109
- source directories.
110
- For a production default, find its declaration before relying on test occurrences. Declarations look like
111
- `default=`, `unwrap_or(`, `const `, `static `, or a settings schema initializer.
112
- For a test-specific default or fixture, inspect its test declaration."""
113
-
114
-
115
- def _main(resources: ExitStack) -> int:
116
- model = os.environ.get("FREELLAMA_TARGET_MODEL") or os.environ["FREELLAMA_BENCH_MODEL"]
117
- workspace = Path(os.environ["FREELLAMA_BENCH_WORKSPACE"]).resolve()
118
- prompt = Path(os.environ["FREELLAMA_BENCH_PROMPT"]).read_text(encoding="utf-8")
119
- result_path = Path(os.environ["FREELLAMA_AGENT_RESULT"])
120
- endpoint = os.environ.get("FREELLAMA_OLLAMA_ENDPOINT", "http://127.0.0.1:11434").rstrip("/")
121
- managed_endpoint = os.environ.get("FREELLAMA_AGENT_MANAGED_ENDPOINT", "").rstrip("/")
122
- execution_preference = os.environ.get("FREELLAMA_AGENT_EXECUTION_PREFERENCE", "auto")
123
- min_placement_evidence = os.environ.get("FREELLAMA_AGENT_MIN_PLACEMENT_EVIDENCE", "configured")
124
- try:
125
- runtime = AgentRuntimeConfig.from_env(default_tool_timeout_seconds=30)
126
- except ValueError as error:
127
- answer = f"invalid research adapter configuration: {error}"
128
- write_failure_result(result_path, answer)
129
- print(answer)
130
- return 1
131
- messages: list[dict[str, Any]] = [
132
- {"role": "system", "content": system_prompt()},
133
- {"role": "user", "content": prompt},
134
- ]
135
- calls: list[dict[str, Any]] = []
136
- usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_tokens": None, "cache_write_tokens": None}
137
- cache_usage = PromptCacheUsage()
138
- metrics = {"load_ms": 0.0, "prompt_eval_ms": 0.0, "eval_ms": 0.0}
139
- execution_receipts: list[dict[str, Any]] = []
140
- answer = ""
141
- failure: str | None = None
142
- transport_error: dict[str, Any] | None = None
143
- calibration_dir = os.environ.get("FREELLAMA_AGENT_TOKEN_CALIBRATION_DIR", "").strip()
144
- calibration_model_identity = (
145
- resolve_model_identity(managed_endpoint or endpoint, model, messages[0]["content"])
146
- if calibration_dir else {"verified": False, "scope": "persistence_disabled", "identity": ""}
147
- )
148
- context_manager = AgentContextManager(
149
- runtime,
150
- model=model,
151
- calibration_identity=calibration_model_identity["identity"],
152
- calibration_dir=Path(calibration_dir) if calibration_dir and calibration_model_identity["verified"] else None,
153
- )
154
- try:
155
- messages = context_manager.fit(messages)
156
- except ValueError as error:
157
- answer = f"research task does not fit the configured context safely: {error}"
158
- write_failure_result(result_path, answer)
159
- print(answer)
160
- return 1
161
- chat_options = {
162
- "temperature": runtime.temperature,
163
- "seed": runtime.seed,
164
- "num_ctx": runtime.num_ctx,
165
- "num_predict": runtime.num_predict,
166
- }
167
-
168
- def call_model() -> dict[str, Any]:
169
- # Retry explicit overload/connection refusals only. Replaying a timeout or an uncertain
170
- # transport failure can overlap a generation that is still consuming the runner.
171
- last_error: Exception | None = None
172
- for attempt in range(runtime.retry_attempts):
173
- try:
174
- transport_endpoint = (
175
- f"{managed_endpoint}/_freellama/v1/tasks" if managed_endpoint else endpoint
176
- )
177
- url, payload = chat_request(
178
- transport_endpoint,
179
- model,
180
- messages,
181
- chat_options,
182
- runtime.think,
183
- runtime.keep_alive,
184
- execution_preference,
185
- min_placement_evidence,
186
- )
187
- response = unwrap_chat_response(
188
- request_json(url, payload, runtime.request_timeout_seconds), execution_receipts
189
- )
190
- break
191
- except (HTTPError, URLError, TimeoutError) as error:
192
- if not retryable_chat_error(error):
193
- raise
194
- last_error = error
195
- if attempt + 1 < runtime.retry_attempts:
196
- time.sleep(runtime.retry_backoff_seconds)
197
- else:
198
- raise last_error # type: ignore[misc]
199
- usage["input_tokens"] += int(response.get("prompt_eval_count", 0))
200
- cache_usage.observe(response)
201
- usage["cache_read_tokens"] = cache_usage.tokens
202
- usage["output_tokens"] += int(response.get("eval_count", 0))
203
- metrics["load_ms"] += float(response.get("load_duration", 0)) / 1_000_000
204
- metrics["prompt_eval_ms"] += float(response.get("prompt_eval_duration", 0)) / 1_000_000
205
- metrics["eval_ms"] += float(response.get("eval_duration", 0)) / 1_000_000
206
- context_manager.observe(messages, response.get("prompt_eval_count"))
207
- return response
208
-
209
- def refit() -> bool:
210
- nonlocal messages, failure
211
- try:
212
- messages = context_manager.fit(messages)
213
- return True
214
- except ValueError as error:
215
- failure = f"research context cannot be fitted safely: {error}"
216
- return False
217
-
218
- seen_calls: dict[str, int] = {}
219
- observations = resources.enter_context(ObservationStore(runtime.context.observation_page_chars))
220
- shell = resources.enter_context(ReadOnlyShell(workspace))
221
- parse_failures = 0
222
- for _ in range(runtime.max_turns):
223
- # Transport failures are terminal (call_model already retried them). A *parse* failure is
224
- # not: tell the model what was wrong with its reply and let it correct itself, keeping every
225
- # tool result gathered so far.
226
- try:
227
- response = call_model()
228
- except (HTTPError, URLError, TimeoutError) as error:
229
- diagnostic, transport_error = chat_error_details(error)
230
- failure = f"agent response failed: {diagnostic}"
231
- break
232
- raw = str(response.get("message", {}).get("content", ""))
233
- try:
234
- action = parse_action(raw)
235
- except (ValueError, json.JSONDecodeError) as error:
236
- parse_failures += 1
237
- if parse_failures > runtime.max_parse_repairs:
238
- failure = f"agent gave {parse_failures} unparseable replies: {type(error).__name__}: {error}"
239
- break
240
- messages.append({"role": "assistant", "content": raw[: runtime.parse_repair_echo_chars]})
241
- messages.append({"role": "user", "content": PARSE_REPAIR_NOTICE})
242
- if not refit():
243
- break
244
- continue
245
- parse_failures = 0
246
- # Paging re-serves stored output. It costs a turn but never a subprocess, and no byte of
247
- # any observation is ever unreachable — which is the whole point of paginating instead of
248
- # clipping.
249
- if action["action"] == "page":
250
- try:
251
- want_step = int(action.get("step", 0))
252
- want_page = int(action.get("page", 1))
253
- except (TypeError, ValueError):
254
- want_step, want_page = 0, 1
255
- messages.append({"role": "assistant", "content": json.dumps(action, ensure_ascii=False)})
256
- messages.append({"role": "user", "content": observations.message(want_step, want_page)})
257
- if not refit():
258
- break
259
- continue
260
- if action["action"] == "finish":
261
- answer = str(action.get("answer", "")).strip()
262
- break
263
- started = time.perf_counter()
264
- command_text = str(action.get("command", ""))
265
- # An exact repeat costs a turn out of ten AND a second copy of an observation the model has
266
- # already seen. Answer it from the prior step instead of re-running the command.
267
- signature = call_signature({"command": command_text})
268
- if signature in seen_calls:
269
- observation = REPEAT_NOTICE
270
- status = "repeat"
271
- else:
272
- try:
273
- observation = run_shell(workspace, command_text, runtime.tool_timeout_seconds, shell)
274
- status = "ok"
275
- except (OSError, ValueError, subprocess.TimeoutExpired) as error:
276
- observation = f"tool error: {type(error).__name__}: {error}"
277
- status = "error"
278
- seen_calls[signature] = len(calls) + 1
279
- calls.append({
280
- "name": "shell",
281
- "raw_name": action.get("action", ""),
282
- "arguments": {"command": command_text},
283
- "status": status,
284
- "duration_ms": round((time.perf_counter() - started) * 1000, 3),
285
- })
286
- messages.append({"role": "assistant", "content": json.dumps(action, ensure_ascii=False)})
287
- remaining = runtime.max_turns - len(calls)
288
- step = len(calls)
289
- observations.put(step, observation, metadata={"action": "shell", "status": status, "source": command_text})
290
- del observation
291
- messages.append({"role": "user", "content": observations.message(step) + f"\n\nCommands remaining: {remaining}. Finish now if the task is answerable; do not repeat prior commands."})
292
- if not refit():
293
- break
294
- else:
295
- messages.append({"role": "user", "content": "Command budget is exhausted. Return exactly a finish JSON action now using the evidence collected. Do not request another command."})
296
- if refit():
297
- try:
298
- response = call_model()
299
- final_action = parse_action(str(response.get("message", {}).get("content", "")))
300
- if final_action.get("action") != "finish":
301
- raise ValueError("forced final response was not finish")
302
- answer = str(final_action.get("answer", "")).strip()
303
- except (HTTPError, URLError, TimeoutError, ValueError, json.JSONDecodeError) as error:
304
- diagnostic, transport_error = chat_error_details(error)
305
- failure = f"agent exceeded {runtime.max_turns} turns and finalization failed: {diagnostic}"
306
-
307
- if not answer:
308
- answer = failure or "agent stopped without a final answer"
309
- result = {
310
- "final_answer": answer,
311
- "tool_calls": calls,
312
- "usage": usage,
313
- "provider_metrics": metrics,
314
- "model_metadata": {
315
- "adapter": "bash_shell_agent_v1",
316
- "sandbox": shell.kind,
317
- "temperature": chat_options["temperature"],
318
- "seed": chat_options["seed"],
319
- "num_ctx": chat_options["num_ctx"],
320
- "num_predict": chat_options["num_predict"],
321
- "max_turns": runtime.max_turns,
322
- "runtime_config": runtime.metadata(),
323
- "context_management": context_manager.metadata(),
324
- "context_compactions": context_manager.compactions,
325
- "transport": "managed" if managed_endpoint else "direct_ollama",
326
- "execution_preference": execution_preference,
327
- "min_placement_evidence": min_placement_evidence,
328
- "execution_receipts": execution_receipts,
329
- **({"transport_error": transport_error} if transport_error else {}),
330
- "cache_token_metrics": cache_usage.metadata(),
331
- "calibration_model_identity": calibration_model_identity,
332
- },
333
- }
334
- observations.write_result(result_path, result)
335
- print(answer)
336
- return 1 if failure else 0
337
-
338
-
339
- def main() -> int:
340
- with ExitStack() as resources:
341
- return _main(resources)
342
-
343
-
344
- if __name__ == "__main__":
345
- raise SystemExit(main())