harness-sdk-python 0.8.0__tar.gz → 0.10.0__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.
Files changed (31) hide show
  1. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/PKG-INFO +1 -1
  2. harness_sdk_python-0.10.0/examples/__init__.py +0 -0
  3. harness_sdk_python-0.10.0/examples/runs_app.py +203 -0
  4. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/pyproject.toml +1 -1
  5. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/linear_thread.py +0 -4
  6. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/run_manager.py +72 -84
  7. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/run_helpers.py +60 -7
  8. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_ack_visibility.py +2 -2
  9. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_batches.py +5 -2
  10. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_branch_anchor.py +46 -6
  11. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_dispatching.py +2 -2
  12. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_edit_dispatched.py +6 -6
  13. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_edit_reload.py +107 -19
  14. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_enqueue.py +13 -12
  15. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_facade.py +9 -6
  16. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_input_required.py +15 -4
  17. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_linear_thread.py +2 -11
  18. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_meta.py +5 -5
  19. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_placement.py +4 -4
  20. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_prepare_hooks.py +10 -18
  21. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_rewind_during_run.py +18 -9
  22. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_settle.py +27 -32
  23. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_steer.py +6 -6
  24. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_stop_continue.py +36 -24
  25. harness_sdk_python-0.8.0/tests/test_run_leaf.py +0 -191
  26. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/.gitignore +0 -0
  27. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/README.md +0 -0
  28. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/__init__.py +0 -0
  29. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/fenced_postgres.py +0 -0
  30. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_fenced_postgres.py +0 -0
  31. {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_outcomes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harness-sdk-python
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: RunManager: the harness-sdk runs subsystem for Python Statewire hosts
5
5
  Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
6
6
  License-Expression: MIT
File without changes
@@ -0,0 +1,203 @@
1
+ """Minimal RunManager backend: a no-tools deepagents agent behind statewire.
2
+
3
+ Run from python/harness-sdk-python:
4
+
5
+ uv run --extra deepagents uvicorn examples.runs_app:app --port 8000
6
+
7
+ State is ``{"messages": [...]}`` in langchain format; rewind truncates the
8
+ list. Threads live at /threads/{id}: ``GET .../stream`` (SSE), ``.../ws``,
9
+ ``POST .../commands``, and ``POST .../assistant-transport/api/chat``.
10
+
11
+ Set PINBOARD_URL, PINBOARD_TOKEN, and ADVERTISE_URL to register with a
12
+ pinboard control plane instead of local placement.
13
+ """
14
+
15
+ import asyncio
16
+ import contextlib
17
+ import os
18
+ from typing import Any
19
+
20
+ from fastapi import FastAPI
21
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
22
+ from pinned import PinnedHost
23
+ from statewire import AssistantTransport, command, plain
24
+ from statewire.langgraph import append_langgraph_event
25
+
26
+ from harness_sdk import RunManager, linear_thread
27
+
28
+
29
+ def select_model() -> str:
30
+ if model := os.environ.get("DEEPAGENTS_MODEL"):
31
+ return model
32
+ if os.environ.get("OPENAI_API_KEY"):
33
+ return "openai:gpt-4o"
34
+ return "anthropic:claude-opus-4-8"
35
+
36
+
37
+ def build_agent() -> Any:
38
+ from deepagents import create_deep_agent
39
+ from langgraph.checkpoint.memory import MemorySaver
40
+
41
+ return create_deep_agent(
42
+ model=select_model(),
43
+ tools=[],
44
+ system_prompt="You are a concise assistant.",
45
+ checkpointer=MemorySaver(),
46
+ )
47
+
48
+
49
+ def to_langchain(message: dict[str, Any]) -> Any:
50
+ match message["type"]:
51
+ case "human":
52
+ return HumanMessage(id=message["id"], content=message["content"])
53
+ case "ai":
54
+ return AIMessage(
55
+ id=message["id"],
56
+ content=message["content"],
57
+ tool_calls=message.get("tool_calls") or [],
58
+ )
59
+ case "tool":
60
+ return ToolMessage(
61
+ id=message["id"],
62
+ content=message["content"],
63
+ tool_call_id=message["tool_call_id"],
64
+ )
65
+ case other:
66
+ raise ValueError(f"unsupported message type: {other}")
67
+
68
+
69
+ def user_text(message: dict[str, Any]) -> str:
70
+ return "\n\n".join(
71
+ part["text"] for part in message["parts"] if part["type"] == "text"
72
+ )
73
+
74
+
75
+ class RunsHost(AssistantTransport):
76
+ assistant_transport_protocol = "assistant-transport"
77
+
78
+ async def lifespan(self):
79
+ self.agent = build_agent()
80
+ self.state = {"messages": []}
81
+ self.thread = linear_thread(
82
+ messages=lambda: plain(self.state["messages"]),
83
+ role=lambda m: "user" if m["type"] == "human" else "assistant",
84
+ )
85
+ self.turn = 0
86
+ self.runs = RunManager(
87
+ state=self.state,
88
+ run=self.run,
89
+ thread=self.thread,
90
+ create_task=self.create_task,
91
+ schedule=self.schedule,
92
+ capabilities=("rewind",),
93
+ )
94
+ yield
95
+
96
+ async def get_message_child_id(self, parent_id: str) -> str | None:
97
+ return await self.thread.get_message_child_id(parent_id)
98
+
99
+ async def run(self, ctx: RunManager.RunContext) -> Any:
100
+ if ctx.has_rollback:
101
+ self.truncate(ctx.rollback_to)
102
+ pending = list(ctx.messages)
103
+ while True:
104
+ for message in pending:
105
+ self.state["messages"].append(
106
+ {
107
+ "id": message["id"],
108
+ "type": "human",
109
+ "content": user_text(message),
110
+ }
111
+ )
112
+ ctx.ack()
113
+ outcome = await self.stream_turn(ctx)
114
+ if outcome is not None:
115
+ return outcome
116
+ pending = list(ctx.steering.take())
117
+ if not pending:
118
+ return RunManager.Complete()
119
+
120
+ def truncate(self, message_id: str | None) -> None:
121
+ messages = self.state["messages"]
122
+ keep = 0
123
+ if message_id is not None:
124
+ items = plain(messages)
125
+ keep = 1 + next(
126
+ index for index, m in enumerate(items) if m["id"] == message_id
127
+ )
128
+ while len(plain(messages)) > keep:
129
+ messages.pop()
130
+
131
+ async def stream_turn(self, ctx: RunManager.RunContext) -> Any:
132
+ history = [to_langchain(m) for m in plain(self.state["messages"])]
133
+ self.turn += 1
134
+ stream = self.agent.astream(
135
+ {"messages": history},
136
+ config={"configurable": {"thread_id": f"{self.id}:{self.turn}"}},
137
+ stream_mode=["messages", "updates"],
138
+ subgraphs=True,
139
+ )
140
+ iterator = stream.__aiter__()
141
+ stop = asyncio.ensure_future(ctx.stop_requested.wait())
142
+ try:
143
+ while True:
144
+ step = asyncio.ensure_future(anext(iterator))
145
+ await asyncio.wait({step, stop}, return_when=asyncio.FIRST_COMPLETED)
146
+ if stop.done() and not step.done():
147
+ step.cancel()
148
+ with contextlib.suppress(asyncio.CancelledError):
149
+ await step
150
+ return RunManager.Stop(dispatch_queue=True)
151
+ try:
152
+ namespace, mode, payload = await step
153
+ except StopAsyncIteration:
154
+ return None
155
+ append_langgraph_event(self.state, namespace, mode, payload)
156
+ finally:
157
+ stop.cancel()
158
+ with contextlib.suppress(Exception):
159
+ await stream.aclose()
160
+
161
+ @command("run/enqueue")
162
+ async def run_enqueue(self, params, *, ctx):
163
+ return await self.runs.enqueue(params, ack=ctx.ack)
164
+
165
+ @command("run/steer")
166
+ async def run_steer(self, params, *, ctx):
167
+ return await self.runs.steer(params, ack=ctx.ack)
168
+
169
+ @command("run/dequeue")
170
+ async def run_dequeue(self, params):
171
+ return await self.runs.dequeue(params)
172
+
173
+ @command("run/edit")
174
+ async def run_edit(self, params, *, ctx):
175
+ return await self.runs.edit(params, ack=ctx.ack)
176
+
177
+ @command("run/reload")
178
+ async def run_reload(self, params, *, ctx):
179
+ return await self.runs.reload(params, ack=ctx.ack)
180
+
181
+ @command("run/stop")
182
+ async def run_stop(self, params=None, *, ctx):
183
+ return await self.runs.stop(params, ack=ctx.ack)
184
+
185
+ @command("run/continue")
186
+ async def run_continue(self, params=None, *, ctx):
187
+ return await self.runs.continue_run(ack=ctx.ack)
188
+
189
+ @command("run/input")
190
+ async def run_input(self, params):
191
+ return await self.runs.input(params)
192
+
193
+
194
+ _pinboard_url = os.environ.get("PINBOARD_URL")
195
+ router = PinnedHost(
196
+ RunsHost,
197
+ pinboard_url=_pinboard_url,
198
+ token=os.environ.get("PINBOARD_TOKEN"),
199
+ advertise_url=os.environ.get("ADVERTISE_URL"),
200
+ namespace="/threads" if _pinboard_url else None,
201
+ )
202
+ app = FastAPI(lifespan=router.lifespan)
203
+ app.include_router(router, prefix="/threads")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "harness-sdk-python"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,10 +21,6 @@ class LinearThread:
21
21
  }
22
22
  return None
23
23
 
24
- async def get_leaf_message_id(self) -> str | None:
25
- items = self._messages()
26
- return items[-1].get("id") if items else None
27
-
28
24
  async def get_message_child_id(self, parent_id: str) -> str | None:
29
25
  """The id of the first non-tool message after parent_id; None when parent_id is unknown or only tools follow."""
30
26
  items = self._messages()