agentbuildkit 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 (58) hide show
  1. agentbuildkit/__init__.py +7 -0
  2. agentbuildkit/__main__.py +6 -0
  3. agentbuildkit/agents/__init__.py +21 -0
  4. agentbuildkit/agents/boundary.py +350 -0
  5. agentbuildkit/cli.py +140 -0
  6. agentbuildkit/config/__init__.py +23 -0
  7. agentbuildkit/config/settings.py +112 -0
  8. agentbuildkit/demo/__init__.py +9 -0
  9. agentbuildkit/demo/models.py +61 -0
  10. agentbuildkit/demo/present.py +79 -0
  11. agentbuildkit/demo/scenario.py +554 -0
  12. agentbuildkit/evaluation/__init__.py +35 -0
  13. agentbuildkit/evaluation/assertions.py +224 -0
  14. agentbuildkit/evaluation/models.py +142 -0
  15. agentbuildkit/evaluation/runner.py +165 -0
  16. agentbuildkit/evaluation/trace.py +46 -0
  17. agentbuildkit/external_execution/__init__.py +98 -0
  18. agentbuildkit/external_execution/binding.py +756 -0
  19. agentbuildkit/external_execution/models.py +171 -0
  20. agentbuildkit/external_execution/provider.py +184 -0
  21. agentbuildkit/external_execution/service.py +787 -0
  22. agentbuildkit/guardrails/__init__.py +27 -0
  23. agentbuildkit/guardrails/approval.py +500 -0
  24. agentbuildkit/integrations/__init__.py +100 -0
  25. agentbuildkit/integrations/cursor.py +394 -0
  26. agentbuildkit/integrations/github.py +1165 -0
  27. agentbuildkit/integrations/http.py +366 -0
  28. agentbuildkit/mcp/__init__.py +25 -0
  29. agentbuildkit/mcp/registry.py +282 -0
  30. agentbuildkit/observability/__init__.py +27 -0
  31. agentbuildkit/observability/audit.py +121 -0
  32. agentbuildkit/observability/correlation.py +23 -0
  33. agentbuildkit/observability/telemetry.py +50 -0
  34. agentbuildkit/orchestrations/__init__.py +24 -0
  35. agentbuildkit/providers/__init__.py +38 -0
  36. agentbuildkit/providers/anthropic.py +60 -0
  37. agentbuildkit/providers/base.py +59 -0
  38. agentbuildkit/providers/openai.py +48 -0
  39. agentbuildkit/providers/registry.py +42 -0
  40. agentbuildkit/py.typed +0 -0
  41. agentbuildkit/runtime.py +39 -0
  42. agentbuildkit/state/__init__.py +91 -0
  43. agentbuildkit/state/api.py +641 -0
  44. agentbuildkit/state/github.py +1419 -0
  45. agentbuildkit/state/models.py +886 -0
  46. agentbuildkit/state/registry.py +3721 -0
  47. agentbuildkit/testing/__init__.py +5 -0
  48. agentbuildkit/testing/scripted_chat.py +107 -0
  49. agentbuildkit/workflows/__init__.py +107 -0
  50. agentbuildkit/workflows/nesting.py +175 -0
  51. agentbuildkit/workflows/patterns.py +229 -0
  52. agentbuildkit/workflows/persistence.py +1032 -0
  53. agentbuildkit/workflows/runtime.py +457 -0
  54. agentbuildkit-0.1.0.dist-info/METADATA +166 -0
  55. agentbuildkit-0.1.0.dist-info/RECORD +58 -0
  56. agentbuildkit-0.1.0.dist-info/WHEEL +4 -0
  57. agentbuildkit-0.1.0.dist-info/entry_points.txt +2 -0
  58. agentbuildkit-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,7 @@
1
+ """AgentBuildKit harness for reliable AI agents."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("agentbuildkit")
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Run the AgentBuildKit command line with ``python -m agentbuildkit``."""
2
+
3
+ from agentbuildkit.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,21 @@
1
+ """Reusable agent construction and typed execution."""
2
+
3
+ from agentbuildkit.agents.boundary import (
4
+ AgentApprovalRequired,
5
+ AgentDefinition,
6
+ AgentDefinitionError,
7
+ AgentInputError,
8
+ AgentOutputError,
9
+ PlatformAgent,
10
+ create_agent,
11
+ )
12
+
13
+ __all__ = [
14
+ "AgentApprovalRequired",
15
+ "AgentDefinition",
16
+ "AgentDefinitionError",
17
+ "AgentInputError",
18
+ "AgentOutputError",
19
+ "PlatformAgent",
20
+ "create_agent",
21
+ ]
@@ -0,0 +1,350 @@
1
+ """Typed agent creation and execution on Microsoft Agent Framework.
2
+
3
+ The caller supplies the definition and a chat client. This module does not
4
+ construct a provider client. ``provider`` and ``model`` on the definition name
5
+ what the agent asked for. The caller decides whether that client comes from
6
+ the provider registry or from a test double.
7
+ """
8
+
9
+ import copy
10
+ import uuid
11
+ from collections.abc import Awaitable, Callable, Mapping, Sequence
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from agent_framework import (
16
+ Agent,
17
+ AgentResponse,
18
+ AgentSession,
19
+ ChatOptions,
20
+ Content,
21
+ FunctionInvocationContext,
22
+ FunctionMiddleware,
23
+ FunctionTool,
24
+ SupportsChatGetResponse,
25
+ )
26
+ from pydantic import BaseModel, ValidationError
27
+
28
+ from agentbuildkit.guardrails import (
29
+ PreparedToolGuard,
30
+ ToolEffect,
31
+ ToolPolicyError,
32
+ tool_audit_middleware,
33
+ tool_effect,
34
+ )
35
+ from agentbuildkit.observability import (
36
+ AuditEvent,
37
+ AuditLog,
38
+ argument_hash,
39
+ bind_correlation_id,
40
+ reset_correlation_id,
41
+ )
42
+ from agentbuildkit.observability.audit import ApprovalState, AuditOutcome
43
+
44
+
45
+ class AgentInputError(Exception):
46
+ """The caller did not supply the agent's input model."""
47
+
48
+
49
+ class AgentOutputError(Exception):
50
+ """The agent response did not match the output model."""
51
+
52
+
53
+ class AgentDefinitionError(Exception):
54
+ """An agent definition is missing a provider id or a model id."""
55
+
56
+
57
+ class AgentApprovalRequired(Exception):
58
+ """A tool call is waiting for approval, so the run has no final output."""
59
+
60
+ def __init__(self, requests: Sequence[Content]) -> None:
61
+ super().__init__("Agent run is waiting for tool approval.")
62
+ self.requests = list(requests)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class AgentDefinition[InputT: BaseModel, OutputT: BaseModel]:
67
+ """One agent. ``create_agent`` receives the chat client separately.
68
+
69
+ ``provider`` and ``model`` belong to this agent. They are not process
70
+ settings, and they are not the agent's name. The caller still injects the
71
+ chat client, so a test can supply a scripted client for the same definition.
72
+ """
73
+
74
+ name: str
75
+ purpose: str
76
+ instructions: str
77
+ input_type: type[InputT]
78
+ output_type: type[OutputT]
79
+ provider: str
80
+ model: str
81
+ tools: tuple[FunctionTool, ...] = ()
82
+
83
+ def __post_init__(self) -> None:
84
+ object.__setattr__(self, "provider", _require_definition_id(self.provider, "Provider id"))
85
+ object.__setattr__(self, "model", _require_definition_id(self.model, "Model id"))
86
+
87
+
88
+ @dataclass
89
+ class PlatformAgent[InputT: BaseModel, OutputT: BaseModel]:
90
+ """A framework agent plus the typed run helper."""
91
+
92
+ definition: AgentDefinition[InputT, OutputT]
93
+ agent: Agent
94
+ audit_log: AuditLog
95
+
96
+ def create_session(self, *, session_id: str | None = None) -> AgentSession:
97
+ """Create an independent Agent Framework session."""
98
+ return self.agent.create_session(session_id=session_id)
99
+
100
+ async def run(
101
+ self,
102
+ data: InputT,
103
+ *,
104
+ session: AgentSession | None = None,
105
+ correlation_id: str | None = None,
106
+ ) -> OutputT:
107
+ """Validate input, run the agent, and return the parsed output model.
108
+
109
+ This method does not accept additional tools. Only tools declared on the
110
+ definition are registered.
111
+ """
112
+ if not isinstance(data, self.definition.input_type):
113
+ raise AgentInputError(f"Expected input type {self.definition.input_type.__name__}.")
114
+ resolved_correlation_id = correlation_id or uuid.uuid4().hex
115
+ correlation_token = bind_correlation_id(resolved_correlation_id)
116
+ try:
117
+ response = await self.agent.run(
118
+ data.model_dump_json(),
119
+ session=session,
120
+ options={"response_format": self.definition.output_type},
121
+ middleware=[
122
+ tool_audit_middleware(
123
+ self.audit_log,
124
+ correlation_id=resolved_correlation_id,
125
+ agent_name=self.definition.name,
126
+ provider=self.definition.provider,
127
+ model=self.definition.model,
128
+ ),
129
+ ],
130
+ )
131
+ _record_unassigned_tool_calls(
132
+ response,
133
+ names=frozenset(tool.name for tool in self.definition.tools),
134
+ audit_log=self.audit_log,
135
+ correlation_id=resolved_correlation_id,
136
+ agent_name=self.definition.name,
137
+ provider=self.definition.provider,
138
+ model=self.definition.model,
139
+ )
140
+ if response.user_input_requests:
141
+ _record_run(
142
+ self.audit_log,
143
+ correlation_id=resolved_correlation_id,
144
+ agent_name=self.definition.name,
145
+ outcome="pending_approval",
146
+ approval="pending",
147
+ provider=self.definition.provider,
148
+ model=self.definition.model,
149
+ )
150
+ raise AgentApprovalRequired(
151
+ _requests_without_arguments(response.user_input_requests)
152
+ )
153
+ output = _validated_output(self.definition, response)
154
+ except AgentApprovalRequired:
155
+ raise
156
+ except Exception:
157
+ _record_run(
158
+ self.audit_log,
159
+ correlation_id=resolved_correlation_id,
160
+ agent_name=self.definition.name,
161
+ outcome="failed",
162
+ approval="not_required",
163
+ provider=self.definition.provider,
164
+ model=self.definition.model,
165
+ )
166
+ raise
167
+ else:
168
+ _record_run(
169
+ self.audit_log,
170
+ correlation_id=resolved_correlation_id,
171
+ agent_name=self.definition.name,
172
+ outcome="completed",
173
+ approval="not_required",
174
+ provider=self.definition.provider,
175
+ model=self.definition.model,
176
+ )
177
+ return output
178
+ finally:
179
+ reset_correlation_id(correlation_token)
180
+
181
+
182
+ def create_agent[InputT: BaseModel, OutputT: BaseModel](
183
+ definition: AgentDefinition[InputT, OutputT],
184
+ *,
185
+ client: SupportsChatGetResponse,
186
+ audit_log: AuditLog | None = None,
187
+ ) -> PlatformAgent[InputT, OutputT]:
188
+ """Create a framework agent from a caller-supplied definition and client.
189
+
190
+ The client is not built here. Production code asks the provider registry
191
+ for a client. Tests pass a scripted client for the same definition.
192
+ """
193
+ for function_tool in definition.tools:
194
+ _require_prepared_tool(function_tool)
195
+ resolved_log = audit_log or AuditLog()
196
+ options: ChatOptions[None] = {"model": definition.model}
197
+ agent = Agent(
198
+ client,
199
+ definition.instructions,
200
+ name=definition.name,
201
+ description=definition.purpose,
202
+ tools=list(definition.tools),
203
+ default_options=options,
204
+ middleware=[
205
+ _AssignedToolGuard(frozenset(function_tool.name for function_tool in definition.tools)),
206
+ PreparedToolGuard(),
207
+ ],
208
+ )
209
+ return PlatformAgent(
210
+ definition=definition,
211
+ agent=agent,
212
+ audit_log=resolved_log,
213
+ )
214
+
215
+
216
+ class _AssignedToolGuard(FunctionMiddleware):
217
+ """Refuse tool calls that were not declared on the agent."""
218
+
219
+ def __init__(self, names: frozenset[str]) -> None:
220
+ self._names = names
221
+
222
+ async def process(
223
+ self,
224
+ context: FunctionInvocationContext,
225
+ call_next: Callable[[], Awaitable[None]],
226
+ ) -> None:
227
+ if context.function.name not in self._names:
228
+ context.metadata["approval_state"] = "rejected"
229
+ context.result = "Tool is not registered for this agent."
230
+ return
231
+ await call_next()
232
+
233
+
234
+ def _record_unassigned_tool_calls(
235
+ response: AgentResponse[Any],
236
+ *,
237
+ names: frozenset[str],
238
+ audit_log: AuditLog,
239
+ correlation_id: str,
240
+ agent_name: str,
241
+ provider: str,
242
+ model: str,
243
+ ) -> None:
244
+ """Audit tool names the model requested that this agent is not allowed to call.
245
+
246
+ The framework returns an error result for a name that is not registered.
247
+ This records that rejection from the run transcript. It does not invent one.
248
+ """
249
+ for message in response.messages:
250
+ for content in message.contents or []:
251
+ name = content.name
252
+ if content.type != "function_call" or not isinstance(name, str) or name in names:
253
+ continue
254
+ audit_log.record(
255
+ AuditEvent(
256
+ correlation_id=correlation_id,
257
+ agent_name=agent_name,
258
+ tool_name=name,
259
+ approval="rejected",
260
+ outcome="failed",
261
+ argument_hash=argument_hash(_call_arguments(content)),
262
+ provider=provider,
263
+ model=model,
264
+ )
265
+ )
266
+
267
+
268
+ def _call_arguments(content: Content) -> dict[str, Any]:
269
+ if content.type != "function_call":
270
+ return {}
271
+ parsed = content.parse_arguments()
272
+ if not isinstance(parsed, Mapping):
273
+ return {}
274
+ arguments: dict[str, Any] = {}
275
+ for key, value in parsed.items():
276
+ arguments[str(key)] = value
277
+ return arguments
278
+
279
+
280
+ def _requests_without_arguments(requests: Sequence[Content]) -> list[Content]:
281
+ """Keep approval requests without the argument values the model supplied."""
282
+ safe: list[Content] = []
283
+ for request in requests:
284
+ copied = copy.copy(request)
285
+ copied.arguments = None
286
+ function_call = copied.function_call
287
+ if function_call is not None:
288
+ call = copy.copy(function_call)
289
+ call.arguments = None
290
+ copied.function_call = call
291
+ safe.append(copied)
292
+ return safe
293
+
294
+
295
+ def _validated_output[InputT: BaseModel, OutputT: BaseModel](
296
+ definition: AgentDefinition[InputT, OutputT],
297
+ response: AgentResponse[Any],
298
+ ) -> OutputT:
299
+ try:
300
+ value = response.value
301
+ except (ValidationError, ValueError):
302
+ raise AgentOutputError(
303
+ f"Agent '{definition.name}' returned output that does not match "
304
+ f"{definition.output_type.__name__}."
305
+ ) from None
306
+ if not isinstance(value, definition.output_type):
307
+ raise AgentOutputError(
308
+ f"Agent '{definition.name}' returned output that does not match "
309
+ f"{definition.output_type.__name__}."
310
+ )
311
+ return value
312
+
313
+
314
+ def _record_run(
315
+ audit_log: AuditLog,
316
+ *,
317
+ correlation_id: str,
318
+ agent_name: str,
319
+ outcome: AuditOutcome,
320
+ approval: ApprovalState,
321
+ provider: str,
322
+ model: str,
323
+ ) -> None:
324
+ audit_log.record(
325
+ AuditEvent(
326
+ correlation_id=correlation_id,
327
+ agent_name=agent_name,
328
+ tool_name=agent_name,
329
+ approval=approval,
330
+ outcome=outcome,
331
+ argument_hash="agent-run",
332
+ provider=provider,
333
+ model=model,
334
+ )
335
+ )
336
+
337
+
338
+ def _require_definition_id(value: object, label: str) -> str:
339
+ if not isinstance(value, str) or value.strip() == "":
340
+ raise AgentDefinitionError(f"{label} must not be empty.")
341
+ return value.strip()
342
+
343
+
344
+ def _require_prepared_tool(function_tool: FunctionTool) -> None:
345
+ effect = tool_effect(function_tool)
346
+ expected = "always_require" if effect is ToolEffect.SIDE_EFFECT else "never_require"
347
+ if function_tool.approval_mode != expected:
348
+ raise ToolPolicyError(
349
+ f"Tool '{function_tool.name}' effect {effect.value} does not match its approval mode."
350
+ )
agentbuildkit/cli.py ADDED
@@ -0,0 +1,140 @@
1
+ """AgentBuildKit command line.
2
+
3
+ ``demo``, ``doctor``, and ``version`` are the current commands. Add later
4
+ commands as new subparsers without changing the ones that already exist.
5
+ """
6
+
7
+ import argparse
8
+ import asyncio
9
+ import os
10
+ import sqlite3
11
+ import sys
12
+ import traceback
13
+ from collections.abc import Callable, Sequence
14
+
15
+ import agentbuildkit
16
+ from agentbuildkit.demo import DemoError, run_demo
17
+
18
+ _OPENAI_KEY = "OPENAI_API_KEY"
19
+ _ANTHROPIC_KEY = "ANTHROPIC_API_KEY"
20
+
21
+
22
+ def main() -> None:
23
+ """Console-script entry point."""
24
+ raise SystemExit(run(sys.argv[1:]))
25
+
26
+
27
+ def run(argv: Sequence[str]) -> int:
28
+ """Run one AgentBuildKit command and return its exit status."""
29
+ parser = _parser()
30
+ try:
31
+ args = parser.parse_args(list(argv))
32
+ except SystemExit as exc:
33
+ if isinstance(exc.code, int):
34
+ return exc.code
35
+ return 0
36
+ command = args.command
37
+ if command == "version":
38
+ print(f"AgentBuildKit {agentbuildkit.__version__}")
39
+ return 0
40
+ if command == "doctor":
41
+ return doctor()
42
+ if command == "demo":
43
+ return _demo(approve=args.approve, reject=args.reject, verbose=args.verbose)
44
+ print("Unknown command.", file=sys.stderr)
45
+ return 2
46
+
47
+
48
+ def doctor(*, writer: Callable[[str], None] = print) -> int:
49
+ """Report local installation checks. Missing provider keys are not a failure."""
50
+ python = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
51
+ package = agentbuildkit.__version__
52
+ sqlite_ok, sqlite_version = _sqlite()
53
+ writer("AgentBuildKit Doctor")
54
+ writer("")
55
+ writer(_status("Python", python, python != ""))
56
+ writer(_status("AgentBuildKit", package, package != ""))
57
+ writer(_status("SQLite", sqlite_version, sqlite_ok))
58
+ writer("")
59
+ writer("Providers")
60
+ writer(f"{'OpenAI':<18} {_credential(_OPENAI_KEY)}")
61
+ writer(f"{'Anthropic':<18} {_credential(_ANTHROPIC_KEY)}")
62
+ writer("")
63
+ healthy = python != "" and package != "" and sqlite_ok
64
+ if healthy:
65
+ writer("Status: ready for offline demo")
66
+ return 0
67
+ writer("Status: installation check failed")
68
+ return 1
69
+
70
+
71
+ def _parser() -> argparse.ArgumentParser:
72
+ parser = argparse.ArgumentParser(
73
+ prog="agentkit",
74
+ description="AgentBuildKit is a harness for reliable AI agents.",
75
+ )
76
+ commands = parser.add_subparsers(dest="command", required=True)
77
+ commands.add_parser("version", help="Print the AgentBuildKit version.")
78
+ commands.add_parser("doctor", help="Check the local installation.")
79
+ demo = commands.add_parser("demo", help="Run the offline harness demonstration.")
80
+ choice = demo.add_mutually_exclusive_group()
81
+ choice.add_argument(
82
+ "--approve",
83
+ action="store_true",
84
+ help="Approve the side effect without prompting.",
85
+ )
86
+ choice.add_argument(
87
+ "--reject",
88
+ action="store_true",
89
+ help="Reject the side effect without prompting.",
90
+ )
91
+ demo.add_argument("--verbose", action="store_true", help="Show an error traceback.")
92
+ return parser
93
+
94
+
95
+ def _demo(*, approve: bool, reject: bool, verbose: bool) -> int:
96
+ if approve:
97
+ decision: bool | None = True
98
+ elif reject:
99
+ decision = False
100
+ else:
101
+ decision = None
102
+ try:
103
+ result = asyncio.run(run_demo(decision=decision, writer=print))
104
+ except DemoError as exc:
105
+ print(exc)
106
+ return 1
107
+ except Exception:
108
+ print("The demo could not finish.")
109
+ if verbose:
110
+ traceback.print_exc()
111
+ return 1
112
+ return 0 if result.ok else 1
113
+
114
+
115
+ def _status(label: str, detail: str, ok: bool) -> str:
116
+ mark = "✓" if ok else "✗"
117
+ return f"{label:<18} {detail:<16} {mark}"
118
+
119
+
120
+ def _credential(name: str) -> str:
121
+ value = os.environ.get(name)
122
+ if isinstance(value, str) and value.strip() != "":
123
+ return "configured"
124
+ return "not configured"
125
+
126
+
127
+ def _sqlite() -> tuple[bool, str]:
128
+ try:
129
+ connection = sqlite3.connect(":memory:")
130
+ except sqlite3.Error:
131
+ return False, ""
132
+ try:
133
+ row = connection.execute("select sqlite_version()").fetchone()
134
+ except sqlite3.Error:
135
+ return False, ""
136
+ finally:
137
+ connection.close()
138
+ if row is None or not isinstance(row[0], str) or row[0] == "":
139
+ return False, ""
140
+ return True, row[0]
@@ -0,0 +1,23 @@
1
+ """Application settings and secret loading."""
2
+
3
+ from agentbuildkit.config.settings import (
4
+ AppSettings,
5
+ CursorSettings,
6
+ GithubSettings,
7
+ load_app_settings,
8
+ load_cursor_settings,
9
+ load_github_settings,
10
+ load_openai_settings,
11
+ redacted_settings,
12
+ )
13
+
14
+ __all__ = [
15
+ "AppSettings",
16
+ "CursorSettings",
17
+ "GithubSettings",
18
+ "load_app_settings",
19
+ "load_cursor_settings",
20
+ "load_github_settings",
21
+ "load_openai_settings",
22
+ "redacted_settings",
23
+ ]
@@ -0,0 +1,112 @@
1
+ """Load application and OpenAI settings without exposing secret values."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any, TypedDict
5
+
6
+ from agent_framework import SecretString, load_settings
7
+ from agent_framework.openai import OpenAISettings
8
+
9
+ _REDACTED = "**********"
10
+
11
+
12
+ class CursorSettings(TypedDict, total=False):
13
+ """Cursor Cloud Agent settings loaded from the ``CURSOR_`` prefix.
14
+
15
+ ``api_key`` stays a ``SecretString``. Callers unwrap it only when building
16
+ the authorization header.
17
+ """
18
+
19
+ api_key: SecretString | None
20
+
21
+
22
+ class GithubSettings(TypedDict, total=False):
23
+ """GitHub App settings loaded from the ``GITHUB_`` prefix.
24
+
25
+ ``private_key`` stays a ``SecretString``. Callers unwrap it only when signing
26
+ an installation token.
27
+ """
28
+
29
+ app_id: str | None
30
+ installation_id: str | None
31
+ private_key: SecretString | None
32
+
33
+
34
+ class AppSettings(TypedDict, total=False):
35
+ """Process settings loaded from the ``APP_`` prefix.
36
+
37
+ These values are not secrets. Provider credentials are not loaded here.
38
+ """
39
+
40
+ service_name: str | None
41
+ environment: str | None
42
+ enable_console_exporters: bool | None
43
+ otlp_endpoint: str | None
44
+ state_directory: str | None
45
+
46
+
47
+ def load_app_settings(
48
+ *,
49
+ env_file_path: str | None = None,
50
+ **overrides: Any,
51
+ ) -> AppSettings:
52
+ """Load application settings from overrides, an env file, then the process environment."""
53
+ return load_settings(
54
+ AppSettings,
55
+ env_prefix="APP_",
56
+ env_file_path=env_file_path,
57
+ **overrides,
58
+ )
59
+
60
+
61
+ def load_cursor_settings(
62
+ *,
63
+ env_file_path: str | None = None,
64
+ **overrides: Any,
65
+ ) -> CursorSettings:
66
+ """Load Cursor API settings. The API key remains a ``SecretString``."""
67
+ return load_settings(
68
+ CursorSettings,
69
+ env_prefix="CURSOR_",
70
+ env_file_path=env_file_path,
71
+ **overrides,
72
+ )
73
+
74
+
75
+ def load_github_settings(
76
+ *,
77
+ env_file_path: str | None = None,
78
+ **overrides: Any,
79
+ ) -> GithubSettings:
80
+ """Load GitHub App settings. The private key remains a ``SecretString``."""
81
+ return load_settings(
82
+ GithubSettings,
83
+ env_prefix="GITHUB_",
84
+ env_file_path=env_file_path,
85
+ **overrides,
86
+ )
87
+
88
+
89
+ def load_openai_settings(
90
+ *,
91
+ env_file_path: str | None = None,
92
+ **overrides: Any,
93
+ ) -> OpenAISettings:
94
+ """Load OpenAI provider settings.
95
+
96
+ A missing API key stays unset so offline tests can run. These settings
97
+ belong to the OpenAI provider. The rest of the harness does not require them.
98
+ """
99
+ return load_settings(
100
+ OpenAISettings,
101
+ env_prefix="OPENAI_",
102
+ env_file_path=env_file_path,
103
+ **overrides,
104
+ )
105
+
106
+
107
+ def redacted_settings(settings: Mapping[str, Any]) -> dict[str, Any]:
108
+ """Return a copy safe to log. Secret values are replaced with a mask."""
109
+ redacted: dict[str, Any] = {}
110
+ for key, value in settings.items():
111
+ redacted[str(key)] = _REDACTED if isinstance(value, SecretString) else value
112
+ return redacted
@@ -0,0 +1,9 @@
1
+ """Offline harness demonstration shipped with the AgentBuildKit package.
2
+
3
+ This sample uses the harness directly. It is not the customer-service example,
4
+ and it does not call a model provider.
5
+ """
6
+
7
+ from agentbuildkit.demo.scenario import DemoError, DemoResult, run_demo
8
+
9
+ __all__ = ["DemoError", "DemoResult", "run_demo"]