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.
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/PKG-INFO +1 -1
- harness_sdk_python-0.10.0/examples/__init__.py +0 -0
- harness_sdk_python-0.10.0/examples/runs_app.py +203 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/pyproject.toml +1 -1
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/linear_thread.py +0 -4
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/run_manager.py +72 -84
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/run_helpers.py +60 -7
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_ack_visibility.py +2 -2
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_batches.py +5 -2
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_branch_anchor.py +46 -6
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_dispatching.py +2 -2
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_edit_dispatched.py +6 -6
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_edit_reload.py +107 -19
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_enqueue.py +13 -12
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_facade.py +9 -6
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_input_required.py +15 -4
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_linear_thread.py +2 -11
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_meta.py +5 -5
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_placement.py +4 -4
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_prepare_hooks.py +10 -18
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_rewind_during_run.py +18 -9
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_settle.py +27 -32
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_steer.py +6 -6
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_stop_continue.py +36 -24
- harness_sdk_python-0.8.0/tests/test_run_leaf.py +0 -191
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/.gitignore +0 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/README.md +0 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.8.0 → harness_sdk_python-0.10.0}/tests/test_outcomes.py +0 -0
|
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")
|
|
@@ -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()
|