sectr 0.0.4__tar.gz

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.
sectr-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.3
2
+ Name: sectr
3
+ Version: 0.0.4
4
+ Summary: Sectr Python SDK: framework-agnostic agent deployment (decorators + runner-contract adapter)
5
+ Author: Donald Pinckney
6
+ Author-email: Donald Pinckney <donald_pinckney@icloud.com>
7
+ Requires-Dist: fastapi
8
+ Requires-Dist: uvicorn
9
+ Requires-Dist: claude-code-sdk ; extra == 'claude-code'
10
+ Requires-Dist: fastmcp ; extra == 'claude-code'
11
+ Requires-Dist: langgraph ; extra == 'langgraph'
12
+ Requires-Dist: openai-agents>=0.2,<1 ; extra == 'openai-agents'
13
+ Requires-Python: >=3.12
14
+ Provides-Extra: claude-code
15
+ Provides-Extra: langgraph
16
+ Provides-Extra: openai-agents
17
+ Description-Content-Type: text/markdown
18
+
sectr-0.0.4/README.md ADDED
File without changes
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "sectr"
3
+ version = "0.0.4"
4
+ description = "Sectr Python SDK: framework-agnostic agent deployment (decorators + runner-contract adapter)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "fastapi",
9
+ "uvicorn",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "Donald Pinckney"
14
+ email = "donald_pinckney@icloud.com"
15
+
16
+ [project.optional-dependencies]
17
+ openai-agents = ["openai-agents>=0.2,<1"]
18
+ langgraph = ["langgraph"]
19
+ claude-code = [
20
+ "claude-code-sdk",
21
+ "fastmcp",
22
+ ]
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=8",
27
+ "httpx>=0.27",
28
+ "pytest-asyncio>=0.24",
29
+ "jsonschema>=4.23",
30
+ "rfc3339-validator>=0.1.4",
31
+ "mypy>=2.3.1",
32
+ ]
33
+
34
+ [tool.pytest.ini_options]
35
+ asyncio_mode = "auto"
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.9.26,<0.10.0"]
39
+ build-backend = "uv_build"
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "sectr"
3
+ version = "0.0.4"
4
+ description = "Sectr Python SDK: framework-agnostic agent deployment (decorators + runner-contract adapter)"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Donald Pinckney", email = "donald_pinckney@icloud.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ # The FastAPI runner-contract adapter (sectr.server) — base dep (decided
12
+ # 2026-09-10): every HOSTED variant needs it, so it belongs to the SDK's
13
+ # core; the extras exist only for framework imports the core doesn't
14
+ # require.
15
+ "fastapi",
16
+ "uvicorn",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ # Framework adapters (each extra mirrors an examples/ variant).
21
+ openai-agents = ["openai-agents>=0.2,<1"]
22
+ langgraph = ["langgraph"]
23
+ claude-code = ["claude-code-sdk", "fastmcp"]
24
+
25
+ [dependency-groups]
26
+ dev = [
27
+ "pytest>=8",
28
+ "httpx>=0.27",
29
+ "pytest-asyncio>=0.24",
30
+ "jsonschema>=4.23",
31
+ "rfc3339-validator>=0.1.4",
32
+ "mypy>=2.3.1",
33
+ ]
34
+
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.9.26,<0.10.0"]
40
+ build-backend = "uv_build"
@@ -0,0 +1,53 @@
1
+ """Sectr Python SDK: framework-agnostic agent deployment.
2
+
3
+ Core (framework-free): `AgentApp` (route registry), `SessionContext` (the
4
+ handler argument), `sectr.events` (typed events mirroring
5
+ schema/event.schema.json), `sectr.tools` (`@tool` registry + approval
6
+ gating). Handlers are async generators of events; the FastAPI server
7
+ adapter (`sectr.server`) speaks the runner invoke contract, and framework
8
+ adapters (`sectr.adapters.*`) wrap native framework objects into handlers.
9
+
10
+ Contracts: `../schema/` is the single source of truth — see `schema/PLAN.md`
11
+ and `sdk-python/PLAN.md`.
12
+ """
13
+
14
+ from .app import AgentApp, Handler, Route
15
+ from .context import SessionContext
16
+ from .events import (
17
+ Envelope,
18
+ Error,
19
+ MessageDelta,
20
+ MessageEnd,
21
+ MessageStart,
22
+ NoMoreActions,
23
+ RunnerEvent,
24
+ TranscriptCompacted,
25
+ ToolApprovalRequired,
26
+ ToolCall,
27
+ ToolResult,
28
+ NoMoreActions,
29
+ )
30
+ from .tools import ApprovalRequired, Tool, tool
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ __all__ = [
35
+ "AgentApp",
36
+ "ApprovalRequired",
37
+ "Envelope",
38
+ "Error",
39
+ "Handler",
40
+ "MessageDelta",
41
+ "MessageEnd",
42
+ "MessageStart",
43
+ "Route",
44
+ "RunnerEvent",
45
+ "SessionContext",
46
+ "Tool",
47
+ "ToolApprovalRequired",
48
+ "ToolCall",
49
+ "ToolResult",
50
+ "NoMoreActions",
51
+ "TranscriptCompacted",
52
+ "tool",
53
+ ]
@@ -0,0 +1,12 @@
1
+ """Framework adapters (optional extras). Each adapter is a handler factory:
2
+ it takes the framework's native object and returns a
3
+ `(SessionContext) -> AsyncIterator[RunnerEvent]` handler, so registration is
4
+ one line per app:
5
+
6
+ app.add_route("/chat", openai_agents(agent))
7
+ app.add_route("/chat", langgraph(graph))
8
+ app.add_route("/chat", claude_code(options))
9
+
10
+ Mapping details (pause/resume semantics, transcript reconstruction) are
11
+ documented per module and pinned by the variant sketches in `examples/`.
12
+ """
@@ -0,0 +1,45 @@
1
+ """Claude Code adapter (`sectr[claude-code]` extra) — the near-black-box
2
+ case: the CLI owns its loop, tools, and permissions.
3
+
4
+ `claude_code(**options)` returns a handler that (see
5
+ `examples/claude-code/main.py`; session-memory mapping decided in
6
+ `examples/claude-code/PLAN.md`):
7
+
8
+ - runs the `claude` CLI (via claude-code-sdk) with `--resume` keyed to our
9
+ session_id (option (b): the CLI's workspace state persists across turns;
10
+ the journal stays authoritative for approvals, SSE, audit, transcript);
11
+ - assistant text → MESSAGE_*; ToolUseBlock → TOOL_CALL; ToolResultBlock →
12
+ TOOL_RESULT; result message → NO_MORE_ACTIONS (turn over);
13
+ - pause: the CLI's `can_use_tool` permission callback fires → the adapter
14
+ captures (tool_name, input), terminates the subprocess, journals
15
+ TOOL_APPROVAL_REQUIRED as the final frame (turn suspended, no sentinel);
16
+ - resume: `--resume` the same session; when the CLI re-issues the gated
17
+ tool and asks permission, the adapter answers from
18
+ `ctx.decision` (allow/deny). At most ONE pending approval per
19
+ session is possible by the turn model.
20
+
21
+ CI note: the `claude` binary is a shim script emitting canned JSONL
22
+ (examples/claude-code/tests/shim_claude.py). Implementation pending.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from collections.abc import AsyncIterator
28
+ from typing import Any
29
+
30
+ from ..app import Handler
31
+ from ..context import SessionContext
32
+ from ..events import RunnerEvent
33
+
34
+ __all__ = ["claude_code"]
35
+
36
+
37
+ def claude_code(**options: Any) -> Handler:
38
+ """Handler factory wrapping claude-code-sdk options. Implementation
39
+ pending."""
40
+
41
+ async def handler(ctx: SessionContext) -> AsyncIterator[RunnerEvent]:
42
+ raise NotImplementedError("sectr.adapters.claude_code")
43
+ yield # pragma: no cover - makes this an async generator
44
+
45
+ return handler
@@ -0,0 +1,80 @@
1
+ """LangGraph adapter (`sectr[langgraph]` extra).
2
+
3
+ `langgraph(graph)` returns a handler that (see `examples/langgraph/main.py`):
4
+
5
+ - normal turn: transcript → `{"messages": [...]}` state; `graph.astream`
6
+ (stream_mode="messages") → MESSAGE_* / TOOL_CALL / TOOL_RESULT;
7
+ stream end → NO_MORE_ACTIONS (turn over). No LangGraph checkpointer — the journal
8
+ is the checkpoint.
9
+ - pause: the sectr-gated tool raises `ApprovalRequired` inside the tool
10
+ node; the adapter catches it escaping `astream` → journaled
11
+ TOOL_APPROVAL_REQUIRED as the final frame (turn suspended, no sentinel).
12
+ - resume: rebuild messages from the transcript, execute the approved tool
13
+ via the sectr tool wrapper (or denial), append the ToolMessage, continue.
14
+
15
+ Post-MVP option (recorded in sdk-python/PLAN.md): a session-scoped
16
+ checkpointer workspace enables idiomatic `interrupt()` /
17
+ `Command(resume=decision)` tool bodies. Implementation pending.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import AsyncIterator
23
+ from typing import Any, cast
24
+
25
+ from ..app import Handler
26
+ from ..context import SessionContext
27
+ from ..events import RunnerEvent
28
+ from ..tools import ApprovalRequired
29
+
30
+ __all__ = ["langgraph", "as_langgraph_tool"]
31
+
32
+
33
+ def langgraph(graph: Any) -> Handler:
34
+ """Handler factory wrapping a compiled LangGraph. Implementation pending."""
35
+
36
+ async def handler(ctx: SessionContext) -> AsyncIterator[RunnerEvent]:
37
+ raise NotImplementedError("sectr.adapters.langgraph")
38
+ yield # pragma: no cover - makes this an async generator
39
+
40
+ return handler
41
+
42
+
43
+ def as_langgraph_tool(t: Any) -> Any:
44
+ """Convert a sectr `Tool` into a LangGraph/langchain-compatible tool,
45
+ preserving approval gating: the coroutine raises `ApprovalRequired`
46
+ inside the tool node when gated (the adapter catches it escaping
47
+ `astream` and ends the turn). Skeleton implementation — schema inferred
48
+ from the original function signature; golden tests arrive with the
49
+ adapter's functional round."""
50
+ import inspect
51
+
52
+ from langchain_core.tools import StructuredTool
53
+ from pydantic import create_model
54
+
55
+ if t.description is None:
56
+ raise ValueError(f"tool {t.name!r} needs a docstring (used as the LLM description)")
57
+
58
+ params = inspect.signature(t.fn).parameters
59
+ fields = {
60
+ pname: (
61
+ p.annotation if p.annotation is not inspect.Parameter.empty else str,
62
+ ... if p.default is inspect.Parameter.empty else p.default,
63
+ )
64
+ for pname, p in params.items()
65
+ }
66
+ # pydantic's create_model overloads don't cleanly accept a heterogeneous
67
+ # kwargs dict — a cast is honest here; schema correctness is golden-tested
68
+ # with the adapter's functional round.
69
+ args_schema = create_model(f"{t.name}_args", **cast(Any, fields))
70
+
71
+ from ..tools import is_gated
72
+
73
+ async def _run(**kwargs: Any) -> Any:
74
+ if is_gated(t.name):
75
+ raise ApprovalRequired(t.name, kwargs)
76
+ return await t(**kwargs)
77
+
78
+ return StructuredTool.from_function(
79
+ coroutine=_run, name=t.name, description=t.description, args_schema=args_schema
80
+ )