neural-context-protocol 0.1.0a1__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.
ncp/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """Public package surface for Neural Context Protocol."""
2
+
3
+ from .api import agent, configure, emit, get_context, run, stream, write_memory
4
+ from .benchmarks import estimate_tokens, run_coding_pipeline_benchmark, run_research_pipeline_benchmark
5
+ from .dogfood import (
6
+ get_live_provider_readiness,
7
+ load_dogfood_adapter,
8
+ run_adapter_continuation_dogfood_loop,
9
+ run_canonical_dogfood_loop,
10
+ run_canonical_http_dogfood_loop,
11
+ run_live_adapter_continuation_attempt,
12
+ run_repeatability_dogfood_loop,
13
+ )
14
+ from .version import __version__
15
+
16
+ __all__ = [
17
+ "__version__",
18
+ "agent",
19
+ "configure",
20
+ "estimate_tokens",
21
+ "emit",
22
+ "get_live_provider_readiness",
23
+ "get_context",
24
+ "load_dogfood_adapter",
25
+ "run_adapter_continuation_dogfood_loop",
26
+ "run_canonical_dogfood_loop",
27
+ "run_canonical_http_dogfood_loop",
28
+ "run_live_adapter_continuation_attempt",
29
+ "run_repeatability_dogfood_loop",
30
+ "run",
31
+ "run_coding_pipeline_benchmark",
32
+ "run_research_pipeline_benchmark",
33
+ "stream",
34
+ "write_memory",
35
+ ]
@@ -0,0 +1,2 @@
1
+ """Provider adapters package."""
2
+
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from os import environ
5
+
6
+ from ncp.adapters.base import BaseAdapter
7
+
8
+
9
+ class AnthropicAdapter(BaseAdapter):
10
+ @property
11
+ def ctx_window(self) -> int:
12
+ return 200000
13
+
14
+ def __init__(
15
+ self,
16
+ api_key: str = "",
17
+ model: str = "claude-sonnet-4-20250514",
18
+ max_tokens: int = 4096,
19
+ timeout: float = 120.0,
20
+ ) -> None:
21
+ try:
22
+ import anthropic
23
+ except ImportError as err:
24
+ raise ImportError(
25
+ "anthropic is required. Install it with: pip install 'ncp-sdk[providers]'"
26
+ ) from err
27
+ self._anthropic = anthropic
28
+ resolved_key = api_key or environ.get("ANTHROPIC_API_KEY", "")
29
+ self._client = anthropic.Anthropic(
30
+ api_key=self._require_api_key(resolved_key, env_var="ANTHROPIC_API_KEY"),
31
+ timeout=timeout,
32
+ )
33
+ self._model = model
34
+ self._max_tokens = max_tokens
35
+
36
+ def call(self, ncp_context: str, user_turn: str) -> str:
37
+ msg = self._run_provider_call(
38
+ lambda: self._client.messages.create(
39
+ model=self._model,
40
+ max_tokens=self._max_tokens,
41
+ system=ncp_context,
42
+ messages=[{"role": "user", "content": user_turn}],
43
+ ),
44
+ provider="Anthropic",
45
+ timeout_types=(self._anthropic.APITimeoutError, TimeoutError),
46
+ )
47
+ texts = [b.text for b in msg.content if b.type == "text"]
48
+ return self._coerce_text("".join(texts), provider="Anthropic")
49
+
50
+ def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
51
+ stream_ctx = self._run_provider_call(
52
+ lambda: self._client.messages.stream(
53
+ model=self._model,
54
+ max_tokens=self._max_tokens,
55
+ system=ncp_context,
56
+ messages=[{"role": "user", "content": user_turn}],
57
+ ),
58
+ provider="Anthropic",
59
+ timeout_types=(self._anthropic.APITimeoutError, TimeoutError),
60
+ )
61
+ with stream_ctx as stream:
62
+ for event in stream:
63
+ if event.type == "content_block_delta" and event.delta.type == "text_delta":
64
+ yield event.delta.text
ncp/adapters/base.py ADDED
@@ -0,0 +1,76 @@
1
+ """Base adapter contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Iterator
7
+ from typing import Callable, TypeVar
8
+
9
+
10
+ class NCPAdapterError(RuntimeError):
11
+ """Base class for provider adapter failures."""
12
+
13
+
14
+ class NCPAdapterConfigurationError(NCPAdapterError):
15
+ """Raised when an adapter is misconfigured before making a call."""
16
+
17
+
18
+ class NCPAdapterTimeoutError(NCPAdapterError):
19
+ """Raised when a provider call times out."""
20
+
21
+
22
+ class NCPAdapterResponseError(NCPAdapterError):
23
+ """Raised when a provider returns an unusable response."""
24
+
25
+
26
+ _T = TypeVar("_T")
27
+
28
+
29
+ class BaseAdapter(ABC):
30
+ """Minimal provider adapter contract for the first NCP API slice."""
31
+
32
+ @property
33
+ def ctx_window(self) -> int:
34
+ return 200000
35
+
36
+ @abstractmethod
37
+ def call(self, ncp_context: str, user_turn: str) -> str:
38
+ """Return a blocking response for one assembled context."""
39
+
40
+ def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
41
+ """Yield a streamed response for one assembled context.
42
+
43
+ Tier 2 providers override only if they support streaming.
44
+ """
45
+ raise NotImplementedError(
46
+ f"{type(self).__name__} does not support streaming in NCP V1; use blocking call()"
47
+ )
48
+
49
+ def _require_api_key(self, api_key: str, *, env_var: str) -> str:
50
+ if api_key.strip():
51
+ return api_key
52
+ raise NCPAdapterConfigurationError(
53
+ f"{type(self).__name__} requires {env_var}; configure it or pass api_key explicitly"
54
+ )
55
+
56
+ def _coerce_text(self, value: str | None, *, provider: str) -> str:
57
+ text = (value or "").strip()
58
+ if text:
59
+ return text
60
+ raise NCPAdapterResponseError(f"{provider} returned an empty text response")
61
+
62
+ def _run_provider_call(
63
+ self,
64
+ call: Callable[[], _T],
65
+ *,
66
+ provider: str,
67
+ timeout_types: tuple[type[BaseException], ...] = (TimeoutError,),
68
+ ) -> _T:
69
+ try:
70
+ return call()
71
+ except NCPAdapterError:
72
+ raise
73
+ except timeout_types as exc:
74
+ raise NCPAdapterTimeoutError(f"{provider} timed out: {exc}") from exc
75
+ except Exception as exc:
76
+ raise NCPAdapterError(f"{provider} call failed: {exc}") from exc
ncp/adapters/cohere.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from os import environ
4
+
5
+ from ncp.adapters.base import BaseAdapter
6
+
7
+
8
+ class CohereAdapter(BaseAdapter):
9
+ @property
10
+ def ctx_window(self) -> int:
11
+ return 128000
12
+
13
+ def __init__(
14
+ self,
15
+ api_key: str = "",
16
+ model: str = "command-a-03-2025",
17
+ max_tokens: int = 4096,
18
+ timeout: float = 120.0,
19
+ ) -> None:
20
+ try:
21
+ import cohere
22
+ except ImportError as err:
23
+ raise ImportError(
24
+ "cohere is required. Install it with: pip install 'ncp-sdk[providers]'"
25
+ ) from err
26
+ resolved_key = api_key or environ.get("COHERE_API_KEY", "")
27
+ self._client = cohere.Client(
28
+ api_key=self._require_api_key(resolved_key, env_var="COHERE_API_KEY"),
29
+ timeout=timeout,
30
+ )
31
+ self._model = model
32
+ self._max_tokens = max_tokens
33
+
34
+ def call(self, ncp_context: str, user_turn: str) -> str:
35
+ from cohere.types import ChatMessage
36
+
37
+ resp = self._run_provider_call(
38
+ lambda: self._client.chat(
39
+ model=self._model,
40
+ max_tokens=self._max_tokens,
41
+ preamble=ncp_context,
42
+ messages=[
43
+ ChatMessage(role="user", message=user_turn),
44
+ ],
45
+ ),
46
+ provider="Cohere",
47
+ )
48
+ return self._coerce_text(resp.text, provider="Cohere")
ncp/adapters/gemini.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from os import environ
4
+
5
+ from ncp.adapters.base import BaseAdapter
6
+
7
+
8
+ class GeminiAdapter(BaseAdapter):
9
+ @property
10
+ def ctx_window(self) -> int:
11
+ return 1000000
12
+
13
+ def __init__(
14
+ self,
15
+ api_key: str = "",
16
+ model: str = "gemini-2.0-flash",
17
+ timeout: float = 120.0,
18
+ ) -> None:
19
+ try:
20
+ import google.generativeai as genai
21
+ except ImportError as err:
22
+ raise ImportError(
23
+ "google-generativeai is required. Install it with: pip install 'ncp-sdk[providers]'"
24
+ ) from err
25
+ resolved_key = api_key or environ.get("GOOGLE_API_KEY", "")
26
+ genai.configure(api_key=self._require_api_key(resolved_key, env_var="GOOGLE_API_KEY"))
27
+ self._model = genai.GenerativeModel(model)
28
+ self._timeout = timeout
29
+
30
+ def call(self, ncp_context: str, user_turn: str) -> str:
31
+ prompt = f"{ncp_context}\n\n{user_turn}"
32
+ resp = self._run_provider_call(
33
+ lambda: self._model.generate_content(
34
+ prompt,
35
+ request_options={"timeout": self._timeout},
36
+ ),
37
+ provider="Gemini",
38
+ )
39
+ return self._coerce_text(resp.text, provider="Gemini")
ncp/adapters/local.py ADDED
@@ -0,0 +1,27 @@
1
+ """Local deterministic adapter for early dogfood and testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+
7
+ from ncp.adapters.base import BaseAdapter
8
+
9
+
10
+ class LocalAdapter(BaseAdapter):
11
+ """A simple blocking/streaming adapter for the SQLite-first local path."""
12
+
13
+ @property
14
+ def ctx_window(self) -> int:
15
+ return 200000
16
+
17
+ def call(self, ncp_context: str, user_turn: str) -> str:
18
+ return (
19
+ "local_adapter_response\n"
20
+ f"user_turn:{user_turn}\n"
21
+ f"context_chars:{len(ncp_context)}"
22
+ )
23
+
24
+ def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
25
+ response = self.call(ncp_context, user_turn)
26
+ for line in response.splitlines(keepends=True):
27
+ yield line
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from os import environ
4
+
5
+ from ncp.adapters.base import BaseAdapter
6
+
7
+
8
+ class MistralAdapter(BaseAdapter):
9
+ @property
10
+ def ctx_window(self) -> int:
11
+ return 128000
12
+
13
+ def __init__(
14
+ self,
15
+ api_key: str = "",
16
+ model: str = "mistral-large-latest",
17
+ max_tokens: int = 4096,
18
+ timeout: float = 120.0,
19
+ ) -> None:
20
+ try:
21
+ from mistralai.client import Mistral
22
+ except ImportError as err:
23
+ raise ImportError(
24
+ "mistralai is required. Install it with: pip install 'ncp-sdk[providers]'"
25
+ ) from err
26
+ resolved_key = api_key or environ.get("MISTRAL_API_KEY", "")
27
+ self._client = Mistral(
28
+ api_key=self._require_api_key(resolved_key, env_var="MISTRAL_API_KEY"),
29
+ timeout_ms=int(timeout * 1000),
30
+ )
31
+ self._model = model
32
+ self._max_tokens = max_tokens
33
+
34
+ def call(self, ncp_context: str, user_turn: str) -> str:
35
+ resp = self._run_provider_call(
36
+ lambda: self._client.chat.complete(
37
+ model=self._model,
38
+ max_tokens=self._max_tokens,
39
+ messages=[
40
+ {"role": "system", "content": ncp_context},
41
+ {"role": "user", "content": user_turn},
42
+ ],
43
+ ),
44
+ provider="Mistral",
45
+ )
46
+ return self._coerce_text(resp.choices[0].message.content, provider="Mistral")
ncp/adapters/ollama.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from os import environ
4
+
5
+ from ncp.adapters.base import BaseAdapter
6
+
7
+ _DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1"
8
+
9
+
10
+ class OllamaAdapter(BaseAdapter):
11
+ @property
12
+ def ctx_window(self) -> int:
13
+ return 8192
14
+
15
+ def __init__(
16
+ self,
17
+ base_url: str = "",
18
+ model: str = "llama3.1",
19
+ max_tokens: int = 4096,
20
+ timeout: float = 120.0,
21
+ max_retries: int = 2,
22
+ ) -> None:
23
+ try:
24
+ import openai
25
+ except ImportError as err:
26
+ raise ImportError(
27
+ "openai is required. Install it with: pip install 'ncp-sdk[providers]'"
28
+ ) from err
29
+ self._openai = openai
30
+ resolved_base_url = base_url or environ.get("OLLAMA_BASE_URL", _DEFAULT_OLLAMA_BASE_URL)
31
+ self._client = openai.OpenAI(
32
+ base_url=resolved_base_url,
33
+ api_key="ollama",
34
+ timeout=timeout,
35
+ max_retries=max_retries,
36
+ )
37
+ self._model = model
38
+ self._max_tokens = max_tokens
39
+
40
+ def call(self, ncp_context: str, user_turn: str) -> str:
41
+ resp = self._run_provider_call(
42
+ lambda: self._client.chat.completions.create(
43
+ model=self._model,
44
+ max_tokens=self._max_tokens,
45
+ messages=[
46
+ {"role": "system", "content": ncp_context},
47
+ {"role": "user", "content": user_turn},
48
+ ],
49
+ ),
50
+ provider="Ollama",
51
+ timeout_types=(self._openai.APITimeoutError, TimeoutError),
52
+ )
53
+ return self._coerce_text(resp.choices[0].message.content, provider="Ollama")
ncp/adapters/openai.py ADDED
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from os import environ
5
+
6
+ from ncp.adapters.base import BaseAdapter
7
+
8
+ _MODEL_WINDOWS: dict[str, int] = {
9
+ "gpt-4o": 128000,
10
+ "gpt-4o-mini": 128000,
11
+ "o1": 200000,
12
+ "o3-mini": 200000,
13
+ "gpt-4.1": 1047576,
14
+ "gpt-4.1-mini": 1047576,
15
+ "gpt-4.1-nano": 1047576,
16
+ }
17
+
18
+
19
+ class OpenAIAdapter(BaseAdapter):
20
+ def __init__(
21
+ self,
22
+ api_key: str = "",
23
+ model: str = "gpt-4o",
24
+ max_tokens: int = 4096,
25
+ timeout: float = 120.0,
26
+ max_retries: int = 2,
27
+ base_url: str | None = None,
28
+ ) -> None:
29
+ try:
30
+ import openai
31
+ except ImportError as err:
32
+ raise ImportError(
33
+ "openai is required. Install it with: pip install 'ncp-sdk[providers]'"
34
+ ) from err
35
+ self._openai = openai
36
+ resolved_key = api_key or environ.get("OPENAI_API_KEY", "")
37
+ kwargs: dict = {
38
+ "api_key": self._require_api_key(resolved_key, env_var="OPENAI_API_KEY"),
39
+ "timeout": timeout,
40
+ "max_retries": max_retries,
41
+ }
42
+ if base_url is not None:
43
+ kwargs["base_url"] = base_url
44
+ self._client = openai.OpenAI(**kwargs)
45
+ self._model = model
46
+ self._max_tokens = max_tokens
47
+
48
+ @property
49
+ def ctx_window(self) -> int:
50
+ return _MODEL_WINDOWS.get(self._model, 128000)
51
+
52
+ def call(self, ncp_context: str, user_turn: str) -> str:
53
+ resp = self._run_provider_call(
54
+ lambda: self._client.chat.completions.create(
55
+ model=self._model,
56
+ max_tokens=self._max_tokens,
57
+ messages=[
58
+ {"role": "system", "content": ncp_context},
59
+ {"role": "user", "content": user_turn},
60
+ ],
61
+ stream=False,
62
+ ),
63
+ provider="OpenAI",
64
+ timeout_types=(self._openai.APITimeoutError, TimeoutError),
65
+ )
66
+ return self._coerce_text(resp.choices[0].message.content, provider="OpenAI")
67
+
68
+ def stream(self, ncp_context: str, user_turn: str) -> Iterator[str]:
69
+ stream = self._run_provider_call(
70
+ lambda: self._client.chat.completions.create(
71
+ model=self._model,
72
+ max_tokens=self._max_tokens,
73
+ messages=[
74
+ {"role": "system", "content": ncp_context},
75
+ {"role": "user", "content": user_turn},
76
+ ],
77
+ stream=True,
78
+ ),
79
+ provider="OpenAI",
80
+ timeout_types=(self._openai.APITimeoutError, TimeoutError),
81
+ )
82
+ for chunk in stream:
83
+ if chunk.choices and (delta := chunk.choices[0].delta.content):
84
+ yield delta
ncp/api.py ADDED
@@ -0,0 +1,210 @@
1
+ """Public API helpers for the SQLite-first NCP runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import time
7
+
8
+ from ncp.adapters.base import BaseAdapter
9
+ from ncp.adapters.local import LocalAdapter
10
+ from ncp.assembler import Assembler
11
+ from ncp.config import NCPConfig, load_config
12
+ from ncp.stores.sqlite import SQLiteStore
13
+ from ncp.types import BudgetContext, ConsciousBlock, NCPResponse, SubconsciousChunk, Whisper
14
+
15
+ _CONFIG: NCPConfig | None = None
16
+
17
+
18
+ def configure(
19
+ *,
20
+ path: str | Path | None = None,
21
+ cwd: str | Path | None = None,
22
+ env: dict[str, str] | None = None,
23
+ ) -> NCPConfig:
24
+ """Load and cache the active NCP configuration."""
25
+
26
+ global _CONFIG
27
+ _CONFIG = load_config(path=path, cwd=cwd, env=env)
28
+ return _CONFIG
29
+
30
+
31
+ def agent(
32
+ *,
33
+ id: str,
34
+ role: str,
35
+ owns: list[str] | None = None,
36
+ must_not: list[str] | None = None,
37
+ task: str = "idle",
38
+ slot: str = "unassigned",
39
+ intent: str = "maintain_context",
40
+ **overrides: object,
41
+ ) -> ConsciousBlock:
42
+ """Create a conscious-block template through the public API."""
43
+
44
+ payload = {
45
+ "agent_id": id,
46
+ "role": role,
47
+ "owns": owns or [],
48
+ "must_not": must_not or [],
49
+ "task": task,
50
+ "slot": slot,
51
+ "intent": intent,
52
+ **overrides,
53
+ }
54
+ return ConsciousBlock(**payload)
55
+
56
+
57
+ def get_context(
58
+ *,
59
+ agent: ConsciousBlock,
60
+ budget: BudgetContext | None = None,
61
+ query_text: str | None = None,
62
+ store: SQLiteStore | None = None,
63
+ config: NCPConfig | None = None,
64
+ ) -> str:
65
+ """Assemble raw pidgin context for one agent turn."""
66
+
67
+ resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
68
+ resolved_store = store or SQLiteStore(resolved_config.store_path)
69
+ assembler = Assembler(store=resolved_store)
70
+ return assembler.assemble(
71
+ conscious=agent,
72
+ budget=budget or BudgetContext(),
73
+ query_text=query_text,
74
+ ).context
75
+
76
+
77
+ def write_memory(
78
+ chunk: SubconsciousChunk,
79
+ *,
80
+ store: SQLiteStore | None = None,
81
+ config: NCPConfig | None = None,
82
+ ) -> bool:
83
+ """Persist one chunk through the public API."""
84
+
85
+ resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
86
+ resolved_store = store or SQLiteStore(resolved_config.store_path)
87
+ return resolved_store.write(chunk)
88
+
89
+
90
+ def emit(
91
+ whisper: Whisper,
92
+ *,
93
+ store: SQLiteStore | None = None,
94
+ config: NCPConfig | None = None,
95
+ ) -> None:
96
+ """Persist one whisper through the public API."""
97
+
98
+ resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
99
+ resolved_store = store or SQLiteStore(resolved_config.store_path)
100
+ resolved_store.emit_whisper(whisper)
101
+
102
+
103
+ def run(
104
+ *,
105
+ agent: ConsciousBlock,
106
+ turn: str,
107
+ adapter: BaseAdapter | None = None,
108
+ budget: BudgetContext | None = None,
109
+ query_text: str | None = None,
110
+ store: SQLiteStore | None = None,
111
+ config: NCPConfig | None = None,
112
+ ) -> NCPResponse:
113
+ """Run one blocking local-runtime call through an adapter."""
114
+
115
+ resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
116
+ resolved_store = store or SQLiteStore(resolved_config.store_path)
117
+ resolved_budget = budget or BudgetContext()
118
+ resolved_adapter = adapter or LocalAdapter()
119
+ assembler = Assembler(store=resolved_store)
120
+
121
+ start = time.perf_counter()
122
+ assembly = assembler.assemble(
123
+ conscious=agent,
124
+ budget=resolved_budget,
125
+ query_text=query_text or turn,
126
+ ctx_window=resolved_adapter.ctx_window,
127
+ )
128
+ content = resolved_adapter.call(assembly.context, turn)
129
+ response = _build_response(
130
+ agent=agent,
131
+ adapter=resolved_adapter,
132
+ context=assembly.context,
133
+ turn=turn,
134
+ content=content,
135
+ start=start,
136
+ )
137
+ assembler.post_turn(
138
+ conscious=agent,
139
+ response=response,
140
+ result_summary=content.splitlines()[0] if content else "",
141
+ result_full=content,
142
+ )
143
+ return response
144
+
145
+
146
+ def stream(
147
+ *,
148
+ agent: ConsciousBlock,
149
+ turn: str,
150
+ adapter: BaseAdapter | None = None,
151
+ budget: BudgetContext | None = None,
152
+ query_text: str | None = None,
153
+ store: SQLiteStore | None = None,
154
+ config: NCPConfig | None = None,
155
+ ):
156
+ """Yield a streamed response through the adapter."""
157
+
158
+ resolved_config = config or _CONFIG or configure(cwd=Path.cwd())
159
+ resolved_store = store or SQLiteStore(resolved_config.store_path)
160
+ resolved_budget = budget or BudgetContext()
161
+ resolved_adapter = adapter or LocalAdapter()
162
+ assembler = Assembler(store=resolved_store)
163
+ assembly = assembler.assemble(
164
+ conscious=agent,
165
+ budget=resolved_budget,
166
+ query_text=query_text or turn,
167
+ ctx_window=resolved_adapter.ctx_window,
168
+ )
169
+ start = time.perf_counter()
170
+ chunks: list[str] = []
171
+ for chunk in resolved_adapter.stream(assembly.context, turn):
172
+ chunks.append(chunk)
173
+ yield chunk
174
+
175
+ content = "".join(chunks)
176
+ response = _build_response(
177
+ agent=agent,
178
+ adapter=resolved_adapter,
179
+ context=assembly.context,
180
+ turn=turn,
181
+ content=content,
182
+ start=start,
183
+ )
184
+ assembler.post_turn(
185
+ conscious=agent,
186
+ response=response,
187
+ result_summary=content.splitlines()[0] if content else "",
188
+ result_full=content,
189
+ )
190
+
191
+
192
+ def _build_response(
193
+ *,
194
+ agent: ConsciousBlock,
195
+ adapter: BaseAdapter,
196
+ context: str,
197
+ turn: str,
198
+ content: str,
199
+ start: float,
200
+ ) -> NCPResponse:
201
+ return NCPResponse(
202
+ content=content,
203
+ input_tokens=len((context + "\n" + turn).split()),
204
+ output_tokens=len(content.split()),
205
+ cost_usd=0.0,
206
+ model=adapter.__class__.__name__.lower(),
207
+ pipeline_id=agent.pipeline_id,
208
+ turn_id=f"turn_{int(time.time() * 1000)}",
209
+ latency_ms=int((time.perf_counter() - start) * 1000),
210
+ )