ness-agent 0.1.0__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.
Files changed (119) hide show
  1. ness_agent/__init__.py +49 -0
  2. ness_agent/agent.py +473 -0
  3. ness_agent/compaction.py +562 -0
  4. ness_agent/context/__init__.py +0 -0
  5. ness_agent/context/coding_overlay.py +153 -0
  6. ness_agent/context/layers.py +400 -0
  7. ness_agent/context/overlay.py +160 -0
  8. ness_agent/defaults/__init__.py +21 -0
  9. ness_agent/defaults/agents/explore.md +10 -0
  10. ness_agent/graph/__init__.py +0 -0
  11. ness_agent/graph/builder.py +47 -0
  12. ness_agent/graph/helpers.py +114 -0
  13. ness_agent/graph/nodes.py +640 -0
  14. ness_agent/graph/state.py +21 -0
  15. ness_agent/hooks.py +232 -0
  16. ness_agent/instructions/__init__.py +42 -0
  17. ness_agent/instructions/act_mode.py +13 -0
  18. ness_agent/instructions/compaction.py +14 -0
  19. ness_agent/instructions/init_memory.py +9 -0
  20. ness_agent/instructions/l0_harness.py +94 -0
  21. ness_agent/instructions/l1_profile.py +11 -0
  22. ness_agent/instructions/plan_mode.py +30 -0
  23. ness_agent/instructions/reflection.py +32 -0
  24. ness_agent/instructions/subagent.py +22 -0
  25. ness_agent/instructions/thread_summary.py +5 -0
  26. ness_agent/mcp.py +385 -0
  27. ness_agent/memory.py +305 -0
  28. ness_agent/options.py +63 -0
  29. ness_agent/permissions.py +340 -0
  30. ness_agent/persistence.py +767 -0
  31. ness_agent/reflection.py +281 -0
  32. ness_agent/session.py +1163 -0
  33. ness_agent/session_context.py +50 -0
  34. ness_agent/skills.py +81 -0
  35. ness_agent/tools/__init__.py +324 -0
  36. ness_agent/tools/ask.py +116 -0
  37. ness_agent/tools/discover.py +185 -0
  38. ness_agent/tools/fs.py +353 -0
  39. ness_agent/tools/search.py +102 -0
  40. ness_agent/tools/shell.py +663 -0
  41. ness_agent/tools/skill.py +43 -0
  42. ness_agent/tools/subagents.py +574 -0
  43. ness_agent/tools/todo.py +89 -0
  44. ness_agent/tools/web.py +750 -0
  45. ness_agent/tracing/__init__.py +27 -0
  46. ness_agent/tracing/config.py +43 -0
  47. ness_agent/tracing/cost.py +275 -0
  48. ness_agent/tracing/exporters/__init__.py +6 -0
  49. ness_agent/tracing/exporters/console.py +126 -0
  50. ness_agent/tracing/exporters/otlp.py +146 -0
  51. ness_agent/tracing/messages.py +152 -0
  52. ness_agent/tracing/semconv.py +64 -0
  53. ness_agent/tracing/tracer.py +245 -0
  54. ness_agent/types.py +123 -0
  55. ness_agent/utils.py +88 -0
  56. ness_agent/workspace/__init__.py +12 -0
  57. ness_agent/workspace/bootstrap.py +65 -0
  58. ness_agent/workspace/git_context.py +59 -0
  59. ness_agent/workspace/project_context.py +152 -0
  60. ness_agent-0.1.0.dist-info/METADATA +181 -0
  61. ness_agent-0.1.0.dist-info/RECORD +119 -0
  62. ness_agent-0.1.0.dist-info/WHEEL +4 -0
  63. ness_agent-0.1.0.dist-info/entry_points.txt +2 -0
  64. ness_agent-0.1.0.dist-info/licenses/LICENSE +202 -0
  65. ness_cli/__init__.py +4 -0
  66. ness_cli/anthropic_messages.py +491 -0
  67. ness_cli/chat_model.py +244 -0
  68. ness_cli/coding_session.py +703 -0
  69. ness_cli/config.py +370 -0
  70. ness_cli/config_store.py +168 -0
  71. ness_cli/events.py +198 -0
  72. ness_cli/factory.py +173 -0
  73. ness_cli/goal.py +341 -0
  74. ness_cli/headless.py +181 -0
  75. ness_cli/instructions/__init__.py +91 -0
  76. ness_cli/instructions/act_mode.md +13 -0
  77. ness_cli/instructions/compaction.md +14 -0
  78. ness_cli/instructions/goal_generic_repair.md +1 -0
  79. ness_cli/instructions/goal_judge.md +15 -0
  80. ness_cli/instructions/goal_repair.md +10 -0
  81. ness_cli/instructions/init_memory.md +9 -0
  82. ness_cli/instructions/l0_harness.md +94 -0
  83. ness_cli/instructions/persona.md +1 -0
  84. ness_cli/instructions/plan_mode.md +30 -0
  85. ness_cli/instructions/reflection.md +32 -0
  86. ness_cli/instructions/subagent.md +22 -0
  87. ness_cli/instructions/thread_summary.md +5 -0
  88. ness_cli/mentions.py +126 -0
  89. ness_cli/model_catalog.py +257 -0
  90. ness_cli/paths.py +206 -0
  91. ness_cli/prompts.py +61 -0
  92. ness_cli/rollback.py +187 -0
  93. ness_cli/tui/__init__.py +1 -0
  94. ness_cli/tui/app.py +776 -0
  95. ness_cli/tui/chrome.py +336 -0
  96. ness_cli/tui/command_catalog.py +41 -0
  97. ness_cli/tui/commands.py +484 -0
  98. ness_cli/tui/config_flow.py +358 -0
  99. ness_cli/tui/config_registry.py +209 -0
  100. ness_cli/tui/constants.py +15 -0
  101. ness_cli/tui/formatting.py +92 -0
  102. ness_cli/tui/header.py +544 -0
  103. ness_cli/tui/images.py +216 -0
  104. ness_cli/tui/keys.py +302 -0
  105. ness_cli/tui/main.py +334 -0
  106. ness_cli/tui/markdown.py +286 -0
  107. ness_cli/tui/mentions.py +203 -0
  108. ness_cli/tui/models.py +20 -0
  109. ness_cli/tui/pickers.py +564 -0
  110. ness_cli/tui/prompts.py +235 -0
  111. ness_cli/tui/render.py +311 -0
  112. ness_cli/tui/stream.py +177 -0
  113. ness_cli/tui/theme.py +162 -0
  114. ness_cli/tui/tool_display.py +530 -0
  115. ness_cli/tui/transcript.py +802 -0
  116. ness_cli/tui/turn_renderer.py +227 -0
  117. ness_cli/tui/utils.py +71 -0
  118. ness_cli/tui/widgets.py +344 -0
  119. ness_cli/worktree.py +120 -0
ness_agent/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ from ness_agent.agent import NessAgent, NessAgentConfig, AgentSpec
2
+ from ness_agent.session import Session
3
+ from ness_agent.options import (
4
+ NessAgentOptions, MemoryConfig, ModeConfig, SubagentConfig, PermissionRules,
5
+ )
6
+ from ness_agent.types import (
7
+ RunResult, SessionEvent, UsageEvent, ApprovalHandler,
8
+ QuestionHandler,
9
+ PlanTurnHandler, InterruptHandler, ContextPreview,
10
+ aggregate_usage,
11
+ )
12
+ from ness_agent.context.layers import PromptLayers, PromptLayersConfig, AuxPrompts
13
+ from ness_agent.context.overlay import OverlayContext, OverlayProvider, render_overlay_delta, wrap_system_reminder
14
+ from ness_agent.context.coding_overlay import CodingOverlay, NoOverlay
15
+ from ness_agent.graph.state import AgentState
16
+ from ness_agent.utils import message_to_text
17
+ from ness_agent.memory import MemoryBackend, MemoryStore
18
+ from ness_agent.persistence import ThreadStore
19
+ from ness_agent.permissions import PermissionStore
20
+ from ness_agent.hooks import Hook, HookRunner
21
+ from ness_agent.skills import SkillLoader
22
+ from ness_agent.tools import ToolRegistry, coding_tools
23
+ from ness_agent.tracing.cost import CostTracker
24
+ from ness_agent.tracing.config import PricingDict, TracingConfig
25
+ from ness_agent.tracing.tracer import (
26
+ Tracer, NoopTracer, NoopSpan, InMemorySpan, MultiTracer, MultiSpan,
27
+ build_tracer, Span,
28
+ )
29
+ from ness_agent.tracing.cost import TokenUsage
30
+ from ness_agent.workspace import (
31
+ git_worktree_summary, get_project_context, setup_ness_structure,
32
+ )
33
+
34
+ __all__ = [
35
+ "NessAgent", "NessAgentConfig", "AgentSpec", "Session",
36
+ "NessAgentOptions", "MemoryConfig", "ModeConfig", "SubagentConfig", "PermissionRules",
37
+ "RunResult", "SessionEvent", "UsageEvent", "ApprovalHandler", "QuestionHandler",
38
+ "PlanTurnHandler", "InterruptHandler", "ContextPreview", "aggregate_usage",
39
+ "PromptLayers", "PromptLayersConfig", "AuxPrompts",
40
+ "OverlayContext", "OverlayProvider", "render_overlay_delta", "wrap_system_reminder",
41
+ "CodingOverlay", "NoOverlay", "AgentState",
42
+ "message_to_text",
43
+ "MemoryBackend", "MemoryStore", "ThreadStore", "PermissionStore",
44
+ "Hook", "HookRunner", "SkillLoader", "ToolRegistry", "coding_tools",
45
+ "CostTracker", "TokenUsage", "PricingDict",
46
+ "TracingConfig", "Tracer", "NoopTracer", "NoopSpan", "InMemorySpan",
47
+ "MultiTracer", "MultiSpan", "build_tracer", "Span",
48
+ "git_worktree_summary", "get_project_context", "setup_ness_structure",
49
+ ]
ness_agent/agent.py ADDED
@@ -0,0 +1,473 @@
1
+ from __future__ import annotations
2
+ from collections.abc import Callable, Mapping, Sequence
3
+ from dataclasses import dataclass, field
4
+ import uuid
5
+ from pathlib import Path
6
+ from typing import Any, TYPE_CHECKING
7
+ from langchain_core.language_models import BaseChatModel
8
+ from langchain_core.tools import BaseTool
9
+ from langgraph.checkpoint.base import BaseCheckpointSaver
10
+
11
+ from ness_agent.options import (
12
+ NessAgentOptions, MemoryConfig, ModeConfig, SubagentConfig,
13
+ )
14
+ from ness_agent.context.layers import PromptLayers, PromptLayersConfig, AuxPrompts
15
+ from ness_agent.context.overlay import OverlayProvider
16
+ from ness_agent.types import ApprovalHandler, QuestionHandler
17
+ from ness_agent.memory import MemoryBackend, MemoryStore
18
+ from ness_agent.persistence import ThreadStore
19
+ from ness_agent.permissions import PermissionStore
20
+ from ness_agent.hooks import Hook, HookRunner
21
+ from ness_agent.skills import SkillLoader
22
+ from ness_agent.tools import BUILTIN_TOOLS, ToolRegistry
23
+ from ness_agent.utils import normalize_tool
24
+ from ness_agent.tracing.cost import CostTracker
25
+ from ness_agent.tracing.config import TracingConfig
26
+ from ness_agent.tracing.tracer import NoopTracer, Tracer, build_tracer
27
+ if TYPE_CHECKING:
28
+ from ness_agent.session import Session
29
+ from ness_agent.types import (
30
+ InterruptHandler,
31
+ PlanTurnHandler,
32
+ )
33
+
34
+
35
+ def _require_instance(name: str, value: object, expected: type) -> None:
36
+ if value is not None and not isinstance(value, expected):
37
+ raise TypeError(
38
+ f"{name} must be an instance of {expected.__name__}, "
39
+ f"got {type(value).__qualname__}"
40
+ )
41
+
42
+ @dataclass(kw_only=True)
43
+ class AgentSpec:
44
+ """User-facing agent configuration.
45
+
46
+ .. highlight:: python
47
+
48
+ Pass directly to :meth:`NessAgent.from_spec` or let
49
+ ``NessAgent(model=..., prompt=..., **kwargs)`` build one internally.
50
+ Every field may be overridden individually; backends are lazily
51
+ resolved into a :class:`NessAgentConfig` by ``NessAgentConfig.resolve``.
52
+
53
+ **Required fields**
54
+
55
+ ``model``
56
+ The primary chat model that drives the agent loop.
57
+ ``prompt``
58
+ L0–L2 prompt: a :class:`~ness_agent.context.layers.PromptLayers`,
59
+ a :class:`~ness_agent.context.layers.PromptLayersConfig`, or a
60
+ plain dict (matching keys extracted, others ignored).
61
+
62
+ **Tools** (optional — defaults to all SDK built-in tools)
63
+
64
+ ``tools``
65
+ ``None`` or a sequence of ``BaseTool``, plain callables
66
+ (auto-wrapped), or strings naming built-in tools (e.g.
67
+ ``"read"``, ``"grep"``, ``"shell"``).
68
+
69
+ **Optional auxiliary models**
70
+
71
+ ``compaction_model``, ``reflection_model``
72
+ Models for compaction summaries and background reflection.
73
+ Falls back to ``model`` when ``None``.
74
+
75
+ **Behaviours**
76
+
77
+ ``options``
78
+ :class:`NessAgentOptions` — compaction budget, context window,
79
+ approval flag, etc.
80
+ ``overlay``
81
+ ``None`` → default :class:`~ness_agent.context.coding_overlay.CodingOverlay`
82
+ (plan/act, git, todos, compaction, session memory).
83
+ Pass :class:`~ness_agent.context.coding_overlay.NoOverlay` for no L3.
84
+ Custom overlays must subclass
85
+ :class:`~ness_agent.context.overlay.OverlayProvider`.
86
+ ``memory``
87
+ :class:`MemoryConfig` — project / user / session memory paths
88
+ (used when ``memory_store`` is not injected).
89
+ ``memory_store``
90
+ Optional :class:`~ness_agent.memory.MemoryBackend` subclass
91
+ instance. When set, skips constructing the default
92
+ :class:`MemoryStore`.
93
+ ``modes``
94
+ :class:`ModeConfig` for plan/act mode (optional; toggle still works
95
+ with default instruction texts when ``None``).
96
+ ``subagents``
97
+ :class:`SubagentConfig` for the ``spawn_subagent`` tool.
98
+ ``aux_prompts``
99
+ :class:`AuxPrompts` — templates for auxiliary LLM calls.
100
+
101
+ **Filesystem paths**
102
+
103
+ ``skills_dir``
104
+ ``.ness/skills/`` directory (or ``None`` to disable skills).
105
+ ``hooks_config``
106
+ Path to ``hooks.json`` for pre/post tool-use hooks. When ``None``,
107
+ defaults to ``{ness_dir}/hooks.json``.
108
+ ``hooks``
109
+ Optional in-memory :class:`~ness_agent.hooks.Hook` list seeded into
110
+ the runner at resolve (combined with the JSON file).
111
+
112
+ **Runtime hooks**
113
+
114
+ ``approval_handler``, ``question_handler``
115
+ Callbacks for destructive-tool approval and user questions.
116
+
117
+ **Integrations**
118
+
119
+ ``checkpoint_factory``
120
+ Callable returning a langgraph ``BaseCheckpointSaver``
121
+ (``None`` → in-memory ``MemorySaver``).
122
+ ``cost_tracker``
123
+ Pricing-aware :class:`CostTracker` (optional). When ``None``,
124
+ resolved from ``tracing.pricing``.
125
+ ``tracer``
126
+ OpenTelemetry-compatible :class:`Tracer` (optional). When
127
+ ``None``, resolved via :func:`build_tracer` from ``tracing``.
128
+ ``tracing``
129
+ :class:`TracingConfig` — toggles tracing, exporter selection,
130
+ capture options, and per-model pricing for cost estimation.
131
+ """
132
+
133
+ # required
134
+ model: BaseChatModel
135
+ prompt: PromptLayers | PromptLayersConfig | Mapping[str, Any]
136
+
137
+ # tools — defaults to all SDK tools (BUILTIN_TOOLS) when None
138
+ tools: Sequence[BaseTool] | None = None
139
+
140
+ # optional auxiliary models
141
+ compaction_model: BaseChatModel | None = None
142
+ reflection_model: BaseChatModel | None = None
143
+
144
+ # behaviours
145
+ options: NessAgentOptions = field(default_factory=NessAgentOptions)
146
+ overlay: OverlayProvider | None = None
147
+ memory: MemoryConfig = field(default_factory=MemoryConfig)
148
+ memory_store: MemoryBackend | None = None
149
+ modes: ModeConfig | None = None
150
+ subagents: SubagentConfig | None = None
151
+ # Prompt templates for auxiliary model calls
152
+ aux_prompts: AuxPrompts = field(default_factory=AuxPrompts)
153
+
154
+ # specs
155
+ skills_dir: Path | None = None
156
+ hooks_config: Path | None = None
157
+ hooks: Sequence[Hook] | None = None
158
+
159
+ # runtime hooks
160
+ approval_handler: ApprovalHandler | None = None
161
+ question_handler: QuestionHandler | None = None
162
+
163
+ # integrations
164
+ checkpoint_factory: Callable[[], BaseCheckpointSaver] | None = None
165
+ tracing: TracingConfig = field(default_factory=TracingConfig)
166
+ cost_tracker: CostTracker | None = None
167
+ tracer: Tracer | None = None
168
+
169
+
170
+ @dataclass(kw_only=True, eq=False)
171
+ class NessAgentConfig:
172
+ """Fully resolved agent config including backends. Prefer AgentSpec / NessAgent(...)."""
173
+
174
+ # required
175
+ model: BaseChatModel
176
+ tools: Sequence[BaseTool]
177
+ prompts: PromptLayers
178
+
179
+ # optional auxiliary models
180
+ compaction_model: BaseChatModel | None = None
181
+ reflection_model: BaseChatModel | None = None
182
+
183
+ # behaviors
184
+ options: NessAgentOptions = field(default_factory=NessAgentOptions)
185
+ overlay: OverlayProvider | None = None
186
+ memory: MemoryConfig = field(default_factory=MemoryConfig)
187
+ modes: ModeConfig | None = None
188
+ subagents: SubagentConfig | None = None
189
+ aux_prompts: AuxPrompts = field(default_factory=AuxPrompts)
190
+
191
+ # specs
192
+ skills_dir: Path | None = None
193
+ hooks_config: Path | None = None
194
+
195
+ # runtime hooks
196
+ approval_handler: ApprovalHandler | None = None
197
+ question_handler: QuestionHandler | None = None
198
+
199
+ # integrations
200
+ checkpoint_factory: Callable[[], BaseCheckpointSaver] | None = None
201
+ tracing: TracingConfig = field(default_factory=TracingConfig)
202
+
203
+ # agent backends (resolved)
204
+ memory_store: MemoryBackend | None = None
205
+ thread_store: ThreadStore | None = None
206
+ permission_store: PermissionStore | None = None
207
+ hook_runner: HookRunner | None = None
208
+ skill_loader: SkillLoader | None = None
209
+ tool_registry: ToolRegistry | None = None
210
+ cost_tracker: CostTracker
211
+ tracer: Tracer
212
+
213
+ @classmethod
214
+ def resolve(cls, spec: AgentSpec) -> "NessAgentConfig":
215
+ """Resolve a user-facing :class:`AgentSpec` into a ready-to-run config.
216
+
217
+ This is called internally by ``NessAgent.__init__`` and
218
+ ``NessAgent.from_spec``. It:
219
+
220
+ 1. Normalises the ``prompt`` field (dict → ``PromptLayersConfig``).
221
+ 2. Resolves ``project_root`` and ``ness_dir`` paths without
222
+ mutating the caller's ``NessAgentOptions`` instance.
223
+ 3. Normalises every tool via :func:`normalize_tool` (BaseTool,
224
+ callable, or name string).
225
+ 4. Instantiates backend stores (``MemoryStore``, ``ThreadStore``,
226
+ ``PermissionStore``, etc.) so the config is fully wired.
227
+ 5. Installs a default :class:`CodingOverlay` when ``overlay`` is
228
+ ``None``, picking up plan/act templates from ``spec.modes`` if
229
+ provided.
230
+ """
231
+ prompt = spec.prompt
232
+ if isinstance(prompt, Mapping):
233
+ prompts = PromptLayers.from_dict(prompt)
234
+ elif isinstance(prompt, PromptLayersConfig):
235
+ prompts = PromptLayers(prompt)
236
+ else:
237
+ prompts = prompt
238
+
239
+ # Resolve paths without mutating the caller's options instance —
240
+ # reusing one NessAgentOptions across two NessAgent builds in
241
+ # different roots otherwise "sticks" the first agent's paths.
242
+ raw_options = spec.options
243
+ project_root = (raw_options.project_root or Path.cwd()).resolve()
244
+ ness_dir = (raw_options.ness_dir or (project_root / ".ness")).resolve()
245
+ if raw_options.project_root is None or raw_options.ness_dir is None:
246
+ import dataclasses as _dc
247
+ overrides = {}
248
+ if raw_options.project_root is None:
249
+ overrides["project_root"] = project_root
250
+ if raw_options.ness_dir is None:
251
+ overrides["ness_dir"] = ness_dir
252
+ options = _dc.replace(raw_options, **overrides)
253
+ else:
254
+ options = raw_options
255
+
256
+ model_name = (
257
+ getattr(spec.model, "model", None)
258
+ or getattr(spec.model, "model_name", None)
259
+ or ""
260
+ )
261
+
262
+ resolved_tools = [
263
+ normalize_tool(t)
264
+ for t in (spec.tools if spec.tools is not None else BUILTIN_TOOLS)
265
+ ]
266
+
267
+ # Default overlay: CodingOverlay ships with the SDK
268
+ # Pass overlay=NoOverlay() to opt out of L3 entirely.
269
+ overlay = spec.overlay
270
+ if overlay is None:
271
+ from ness_agent.context.coding_overlay import CodingOverlay
272
+ modes_cfg = spec.modes
273
+ overlay = CodingOverlay(
274
+ plans_dir=(
275
+ str(modes_cfg.plans_dir) if modes_cfg and modes_cfg.plans_dir
276
+ else ".ness/plans/"
277
+ ),
278
+ plan_mode_template=modes_cfg.plan_mode_template if modes_cfg else None,
279
+ act_mode_template=modes_cfg.act_mode_template if modes_cfg else None,
280
+ )
281
+
282
+ _require_instance("overlay", overlay, OverlayProvider)
283
+ _require_instance("memory_store", spec.memory_store, MemoryBackend)
284
+ _require_instance("approval_handler", spec.approval_handler, ApprovalHandler)
285
+
286
+ return cls(
287
+ model=spec.model,
288
+ tools=resolved_tools,
289
+ prompts=prompts,
290
+
291
+ compaction_model=spec.compaction_model,
292
+ reflection_model=spec.reflection_model,
293
+
294
+ options=options,
295
+ overlay=overlay,
296
+
297
+ memory=spec.memory,
298
+ modes=spec.modes,
299
+ subagents=spec.subagents,
300
+ aux_prompts=spec.aux_prompts,
301
+ skills_dir=spec.skills_dir,
302
+ hooks_config=spec.hooks_config if spec.hooks_config is not None else ness_dir / "hooks.json",
303
+
304
+ approval_handler=spec.approval_handler,
305
+ question_handler=spec.question_handler,
306
+ checkpoint_factory=spec.checkpoint_factory,
307
+ tracing=spec.tracing,
308
+
309
+ memory_store=(
310
+ spec.memory_store
311
+ if spec.memory_store is not None
312
+ else MemoryStore(
313
+ spec.memory, ness_dir=ness_dir, project_root=project_root
314
+ )
315
+ ),
316
+ thread_store=ThreadStore(
317
+ threads_dir=ness_dir / "threads",
318
+ auto_save=options.auto_save_threads,
319
+ default_model=str(model_name or ""),
320
+ ),
321
+ permission_store=PermissionStore(ness_dir=ness_dir, project_root=project_root),
322
+ hook_runner=HookRunner(
323
+ spec.hooks_config if spec.hooks_config is not None else ness_dir / "hooks.json",
324
+ project_root=project_root,
325
+ hooks=spec.hooks,
326
+ ),
327
+ skill_loader=SkillLoader(spec.skills_dir),
328
+ tool_registry=ToolRegistry(resolved_tools),
329
+ cost_tracker=spec.cost_tracker or CostTracker(pricing=spec.tracing.pricing),
330
+ tracer=spec.tracer or build_tracer(spec.tracing),
331
+ )
332
+
333
+
334
+ class NessAgent:
335
+ """Top-level agent — owns a shared config and creates per-thread sessions.
336
+
337
+ This is the primary entry point for the Ness Agent SDK. Construct
338
+ one with a model and prompt, then call ``.session(thread_id=...)``
339
+ for each conversation thread.
340
+
341
+ **Minimal usage** (everything defaults to a working coding agent)::
342
+
343
+ from ness_agent import NessAgent, PromptLayersConfig
344
+ from langchain_openai import ChatOpenAI
345
+
346
+ agent = NessAgent(model=ChatOpenAI(model="gpt-4o"),
347
+ prompt=PromptLayersConfig())
348
+ session = agent.session(thread_id="proj-1")
349
+ result = await session.run("add rate limiter on /api/login")
350
+
351
+ Tools are optional (all SDK built-ins are loaded when omitted).
352
+ The default :class:`~ness_agent.context.coding_overlay.CodingOverlay`
353
+ provides plan/act mode blocks, git snapshot, compaction notes, todos,
354
+ session memory, and loaded skills out of the box.
355
+ """
356
+
357
+ def __init__(self, *, model, prompt, tools: Sequence[BaseTool] | None = None, **kwargs) -> None:
358
+ """Create an agent.
359
+
360
+ Parameters
361
+ ----------
362
+ model : BaseChatModel
363
+ The primary chat model that drives the agent loop.
364
+ prompt : PromptLayers | PromptLayersConfig | dict
365
+ L0–L2 prompt configuration. Accepts a ``PromptLayers`` instance,
366
+ a ``PromptLayersConfig`` dataclass, or a plain dict of field->value
367
+ (unknown keys ignored).
368
+ tools : iterable of BaseTool / callable / str, optional
369
+ Tools available to the agent. ``None`` (the default) loads all
370
+ SDK built-in tools (:data:`~ness_agent.tools.BUILTIN_TOOLS`).
371
+ Items may be ``BaseTool`` instances, plain callables
372
+ (auto-wrapped), or strings resolved from the built-in tool map.
373
+ **kwargs
374
+ Any remaining :class:`AgentSpec` field such as ``options``,
375
+ ``overlay``, ``modes``, ``aux_prompts``, ``memory``, etc.
376
+ """
377
+ spec = AgentSpec(model=model, tools=tools, prompt=prompt, **kwargs)
378
+ self._config = NessAgentConfig.resolve(spec)
379
+
380
+ @classmethod
381
+ def from_spec(cls, spec: AgentSpec) -> "NessAgent":
382
+ """Build an agent from a pre-built :class:`AgentSpec`.
383
+
384
+ Useful when you want to assemble the spec in one place and
385
+ resolve backends later, or when you need to create multiple
386
+ agents from the same spec::
387
+
388
+ agent = NessAgent.from_spec(AgentSpec(
389
+ model=ChatOpenAI(model="gpt-4o"),
390
+ prompt=PromptLayersConfig(),
391
+ options=NessAgentOptions(context_window=128_000),
392
+ ))
393
+ """
394
+ agent = object.__new__(cls)
395
+ agent._config = NessAgentConfig.resolve(spec)
396
+ return agent
397
+
398
+ @property
399
+ def config(self) -> NessAgentConfig:
400
+ """The fully-resolved :class:`NessAgentConfig` for this agent.
401
+
402
+ Contains the resolved backends (``memory_store``, ``thread_store``,
403
+ ``tool_registry``, etc.) plus the user-supplied fields.
404
+ """
405
+ return self._config
406
+
407
+ def session(
408
+ self,
409
+ *,
410
+ thread_id: str,
411
+ mode: str | None = None,
412
+ metadata: Mapping[str, Any] | None = None,
413
+ git_available: bool | None = None,
414
+ vision: bool | None = None,
415
+ on_plan_turn: "PlanTurnHandler | None" = None,
416
+ on_interrupt: "InterruptHandler | None" = None,
417
+ ) -> Session:
418
+ """Create a runnable :class:`~ness_agent.session.Session` for one thread.
419
+
420
+ Parameters
421
+ ----------
422
+ thread_id : str
423
+ Unique identifier for this conversation thread.
424
+ mode : str, optional
425
+ Initial mode — ``"act"`` or ``"plan"``. Falls back to
426
+ ``config.modes.default`` (or ``"act"``) when ``None``.
427
+ metadata : dict, optional
428
+ Arbitrary key-value pairs surfaced to the L3 overlay provider
429
+ via ``ctx.metadata``. Set ``session.metadata[k] = v`` before
430
+ each ``run()`` call if you want to mutate live.
431
+ git_available : bool, optional
432
+ Whether the project has a git repo. Auto-detected when
433
+ ``None``.
434
+ vision : bool, optional
435
+ Forwards image attachments to the model when ``True``; drops
436
+ to text-only and emits a ``warning`` SessionEvent when ``False``;
437
+ shape-blind (forwards verbatim) when ``None``. See
438
+ :class:`~ness_agent.session.Session`.
439
+ on_plan_turn, on_interrupt
440
+ Per-Session runtime hooks. Stored on the :class:`Session` instance
441
+ (not the shared :class:`NessAgentConfig`) so concurrent threads on
442
+ one agent never clobber each other. See :mod:`ness_agent.types`
443
+ for the handler signatures.
444
+ """
445
+ from ness_agent.session import Session
446
+
447
+ cfg = self._config
448
+ mode = mode or (cfg.modes.default if cfg.modes else "act")
449
+ return Session(
450
+ self,
451
+ thread_id=thread_id,
452
+ mode=mode,
453
+ metadata=dict(metadata or {}),
454
+ git_available=git_available,
455
+ vision=vision,
456
+ on_plan_turn=on_plan_turn,
457
+ on_interrupt=on_interrupt,
458
+ )
459
+
460
+ def new_thread_id(self, prefix: str = "session") -> str:
461
+ """Generate a fresh thread ID string.
462
+
463
+ Parameters
464
+ ----------
465
+ prefix : str
466
+ Prefix for the thread ID (default ``"session"``).
467
+
468
+ Returns
469
+ -------
470
+ str
471
+ A short hex string like ``"session-a1b2c3d4"``.
472
+ """
473
+ return f"{prefix}-{uuid.uuid4().hex[:8]}"