harness-sdk-python 0.26.0__py3-none-any.whl → 0.28.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.
harness_sdk/__init__.py CHANGED
@@ -14,6 +14,14 @@ from .checkpoints import (
14
14
  from .deepagents import DeepagentsMessages
15
15
  from .linear_thread import linear_thread
16
16
  from .run_manager import RunManager, RunManagerDescriptor, run_manager
17
+ from .voice import (
18
+ VoiceManager,
19
+ VoiceManagerDescriptor,
20
+ VoiceProvider,
21
+ end_voice,
22
+ start_voice,
23
+ voice_manager,
24
+ )
17
25
 
18
26
  __all__ = [
19
27
  "CheckpointIndex",
@@ -26,10 +34,16 @@ __all__ = [
26
34
  "RunManager",
27
35
  "RunManagerDescriptor",
28
36
  "Snapshot",
37
+ "VoiceManager",
38
+ "VoiceManagerDescriptor",
39
+ "VoiceProvider",
29
40
  "checkpoint_thread",
41
+ "end_voice",
30
42
  "langgraph_checkpoints",
31
43
  "langgraph_snapshot",
32
44
  "langgraph_switch_values",
33
45
  "linear_thread",
34
46
  "run_manager",
47
+ "start_voice",
48
+ "voice_manager",
35
49
  ]
harness_sdk/deepagents.py CHANGED
@@ -66,6 +66,7 @@ class DeepagentsMessages:
66
66
  )
67
67
  self._checkpointer = checkpointer
68
68
  self._captured: dict[str, Any] | None = None
69
+ self._ephemeral: dict[str, dict[str, Any]] = {}
69
70
 
70
71
  async def boot(
71
72
  self, state: Any, *, index: CheckpointIndex, thread_id: str
@@ -150,6 +151,56 @@ class DeepagentsMessages:
150
151
  """Append harness user messages to the wire state as human entries, deduplicated by id; returns the langgraph turn entries."""
151
152
  return [self._append_user(message) for message in messages]
152
153
 
154
+ async def append_messages(self, messages: list[dict[str, Any]]) -> None:
155
+ """Append ``{id, role, text}`` entries as a new checkpoint on the current tip and restamp; the voice commit path."""
156
+ from langchain_core.messages import AIMessage, HumanMessage
157
+ from langgraph.checkpoint.base import empty_checkpoint
158
+ from langgraph.checkpoint.base.id import uuid6
159
+
160
+ if not messages:
161
+ raise ValueError("messages must be non-empty")
162
+ for message in messages:
163
+ if message["role"] not in ("user", "assistant"):
164
+ raise ValueError(f"unsupported message role: {message['role']!r}")
165
+ entries = [
166
+ (HumanMessage if m["role"] == "user" else AIMessage)(
167
+ id=m["id"], content=m["text"]
168
+ )
169
+ for m in messages
170
+ ]
171
+ parent = {"configurable": {"thread_id": self._thread_id, "checkpoint_ns": ""}}
172
+ saved = await self._checkpointer.aget_tuple(parent)
173
+ if saved is None:
174
+ checkpoint = empty_checkpoint()
175
+ step = -1
176
+ else:
177
+ checkpoint = {**saved.checkpoint}
178
+ step = (saved.metadata or {}).get("step", -1)
179
+ parent["configurable"]["checkpoint_id"] = saved.checkpoint["id"]
180
+ version = self._checkpointer.get_next_version(
181
+ checkpoint["channel_versions"].get("messages"), None
182
+ )
183
+ checkpoint = {
184
+ **checkpoint,
185
+ "id": str(uuid6(clock_seq=-2)),
186
+ "ts": datetime.now(timezone.utc).isoformat(),
187
+ "channel_values": {
188
+ **checkpoint["channel_values"],
189
+ "messages": [
190
+ *checkpoint["channel_values"].get("messages", []),
191
+ *entries,
192
+ ],
193
+ },
194
+ "channel_versions": {
195
+ **checkpoint["channel_versions"],
196
+ "messages": version,
197
+ },
198
+ }
199
+ metadata = {"source": "update", "step": step + 1, "parents": {}}
200
+ await self._checkpointer.aput(parent, checkpoint, metadata, {"messages": version})
201
+ await self._thread.ingest_latest()
202
+ self._restamp()
203
+
153
204
  async def switch_branch(self, branch_id: int) -> None:
154
205
  """Switch to ``branch_id``: its tip becomes the checkpointer's latest checkpoint and the wire state rebaselines."""
155
206
  transcript, lineage = await self._thread.switch_state(branch_id)
@@ -191,6 +242,26 @@ class DeepagentsMessages:
191
242
  )
192
243
  return snapshot
193
244
 
245
+ def upsert_ephemeral_message(self, message: dict[str, Any]) -> None:
246
+ """Render an in-flight ``{id, role, text}`` entry in ``state["messages"]``; an ``append_messages`` of the same id replaces it durably."""
247
+ if message["role"] not in ("user", "assistant"):
248
+ raise ValueError(f"unsupported message role: {message['role']!r}")
249
+ if message["id"] not in self._ephemeral and any(
250
+ m["id"] == message["id"] for m in self._thread.transcript
251
+ ):
252
+ raise ValueError(f"message {message['id']!r} is already durable")
253
+ self._ephemeral[message["id"]] = {
254
+ "id": message["id"],
255
+ "type": "human" if message["role"] == "user" else "ai",
256
+ "content": message["text"],
257
+ }
258
+ self._restamp()
259
+
260
+ def drop_ephemeral_message(self, message_id: str) -> None:
261
+ """Remove an in-flight entry from the ``state["messages"]`` projection."""
262
+ del self._ephemeral[message_id]
263
+ self._restamp()
264
+
194
265
  def _append_user(self, message: dict[str, Any]) -> dict[str, Any]:
195
266
  entry = {"id": message["id"], "type": "human", "content": _user_text(message)}
196
267
  if not any(m["id"] == entry["id"] for m in plain(self._state["messages"])):
@@ -207,4 +278,8 @@ class DeepagentsMessages:
207
278
  for message in self._thread.transcript:
208
279
  branch, siblings = self._thread.sibling_info(message["id"])
209
280
  stamped.append({**message, "branch": {"id": branch, "siblings": siblings}})
281
+ durable = {message["id"] for message in stamped}
282
+ stamped.extend(
283
+ entry for id_, entry in self._ephemeral.items() if id_ not in durable
284
+ )
210
285
  self._replace(stamped)
harness_sdk/voice.py ADDED
@@ -0,0 +1,418 @@
1
+ """VoiceManager: the harness-sdk voice subsystem, one tray per live session.
2
+
3
+ ``@voice_manager()`` on a Statewire host method declares the subsystem: the
4
+ decorated method is the provider factory (``def voice(self, ctx) ->
5
+ VoiceProvider``), ``self.<name>`` is the command surface, and each session
6
+ lives in its own ephemeral tray, minted by ``voice/start`` and deleted when
7
+ the session ends. The host implements ``VoiceManager.Host``: the seed read
8
+ and the turn commit both stay host-owned.
9
+
10
+ ``voice/start {offer}`` seeds the provider from the thread transcript (at
11
+ most 128 messages, tail taken) and projects ``state["voice"] = {clientId,
12
+ status, answer}`` — the SDP answer rides the applied state. The session is
13
+ bound to the issuing client's connection: ``voice/end`` from the owner, a
14
+ provider ``closed`` event, or a connection drop ends it. Provider turn
15
+ events render live in ``state["messages"]`` through
16
+ ``Host.upsert_ephemeral_message`` (message id = turn id); each completed
17
+ turn commits through ``Host.commit_voice_turns`` (serialized, in
18
+ done-order) and the durable entry replaces the ephemeral one under the
19
+ same id, a failed commit keeps its turns for the next flush; session end
20
+ flushes the remaining done turns and drops in-progress turns.
21
+
22
+ Rejections: ``voice-active`` (a session is already live), ``no-voice``
23
+ (no voice manager declared, or ``voice/end`` with no session),
24
+ ``not-voice-owner`` (``voice/end`` from another client),
25
+ ``voice-unavailable`` (the factory or ``provider.start`` raised).
26
+ """
27
+
28
+ import asyncio
29
+ import functools
30
+ import inspect
31
+ import logging
32
+ from dataclasses import dataclass
33
+ from typing import Any, Callable, Literal, Protocol, TypedDict
34
+ from uuid import uuid4
35
+
36
+ from pinned.trays import Tray, TrayDescriptor, TrayMapNamespace
37
+ from statepatch import observable
38
+ from statewire import StatewireReject
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ _SEED_LIMIT = 128
43
+ _SEED_ROLES = ("user", "assistant", "system")
44
+
45
+
46
+ def _reject(reason: str, message: str) -> StatewireReject:
47
+ return StatewireReject(message, payload={"reason": reason})
48
+
49
+
50
+ def _capped_seed(seed: Any) -> "list[VoiceManager.SeedMessage]":
51
+ if not isinstance(seed, list):
52
+ raise TypeError(f"get_voice_seed must return a list, got {seed!r}")
53
+ for message in seed:
54
+ if (
55
+ not isinstance(message, dict)
56
+ or message.get("role") not in _SEED_ROLES
57
+ or not isinstance(message.get("text"), str)
58
+ ):
59
+ raise TypeError(f"seed messages must be {{role, text}}, got {message!r}")
60
+ return seed[-_SEED_LIMIT:]
61
+
62
+
63
+ class VoiceManager(Tray):
64
+ """One live voice session: the provider handle and the in-memory turn
65
+ buffer rendered live in ``state["messages"]``; completed turns commit
66
+ per turn and ended sessions flush and self-delete."""
67
+
68
+ connection_poll = 0.5
69
+
70
+ clientId = observable(None)
71
+ status = observable("connecting")
72
+ answer = observable(None)
73
+
74
+ state = observable(
75
+ {
76
+ "clientId": clientId,
77
+ "status": status,
78
+ "answer": answer,
79
+ }
80
+ )
81
+
82
+ def __init__(self, **kwargs: Any) -> None:
83
+ super().__init__(**kwargs)
84
+ self._provider: "VoiceProvider | None" = None
85
+ self._turns: list[dict[str, Any]] = []
86
+ self._flushing = asyncio.Lock()
87
+ self._ended = False
88
+ self._watcher: "asyncio.Task[Any] | None" = None
89
+
90
+ def items(self):
91
+ yield from ()
92
+
93
+ class SeedMessage(TypedDict):
94
+ """One transcript message handed to the provider as session history."""
95
+
96
+ role: Literal["user", "assistant", "system"]
97
+ text: str
98
+
99
+ @dataclass(frozen=True)
100
+ class ToolRequest:
101
+ call_id: str
102
+ name: str
103
+ arguments: str
104
+
105
+ @dataclass(frozen=True)
106
+ class SessionContext:
107
+ """The provider factory's per-session context."""
108
+
109
+ client_id: str
110
+
111
+ class Events(Protocol):
112
+ """The sink a provider drives; the subsystem hands it to ``start``."""
113
+
114
+ def turn_started(self, turn_id: str, role: Literal["user", "assistant"]) -> None: ...
115
+
116
+ def turn_delta(self, turn_id: str, text: str) -> None: ...
117
+
118
+ def turn_done(self, turn_id: str, text: str) -> None: ...
119
+
120
+ def tool_requested(self, request: "VoiceManager.ToolRequest") -> None: ...
121
+
122
+ def rollover_approaching(self, rollover_id: str, deadline: float) -> None: ...
123
+
124
+ def closed(self, reason: str) -> None: ...
125
+
126
+ class Host(Protocol):
127
+ """The host seam: the thread transcript in, live and committed turns out."""
128
+
129
+ async def get_voice_seed(self) -> "list[VoiceManager.SeedMessage]":
130
+ """The transcript as ``{role, text}`` seed messages, tool/system noise skipped."""
131
+ ...
132
+
133
+ async def commit_voice_turns(self, turns: list[dict[str, Any]]) -> None:
134
+ """Write completed ``{id, role, text}`` turns to the thread's durable transcript."""
135
+ ...
136
+
137
+ def upsert_ephemeral_message(self, message: dict[str, Any]) -> None:
138
+ """Render an in-flight ``{id, role, text}`` turn in ``state["messages"]``; same id updates in place."""
139
+ ...
140
+
141
+ def drop_ephemeral_message(self, message_id: str) -> None:
142
+ """Remove an in-flight turn from the ``state["messages"]`` projection."""
143
+ ...
144
+
145
+ # ─── Session lifecycle ──────────────────────────────────
146
+
147
+ async def _begin(
148
+ self, offer: str, seed: "list[VoiceManager.SeedMessage]", caller: Any,
149
+ applied: Callable[[], None],
150
+ ) -> Any:
151
+ self.clientId.value = caller.client_id
152
+ try:
153
+ provider = await self.handler(
154
+ VoiceManager.SessionContext(client_id=caller.client_id)
155
+ )
156
+ answer = await provider.start(offer, seed=seed, events=self)
157
+ if not isinstance(answer, str) or answer == "":
158
+ raise TypeError(f"provider.start must return an SDP answer, got {answer!r}")
159
+ except Exception as exc:
160
+ self._ended = True
161
+ self.delete()
162
+ raise _reject("voice-unavailable", f"voice session failed to start: {exc}")
163
+ self._provider = provider
164
+ self.answer.value = answer
165
+ self.status.value = "live"
166
+ applied()
167
+ self._watcher = self._spawn(self._watch(caller))
168
+ return {"answer": answer}
169
+
170
+ async def _watch(self, caller: Any) -> None:
171
+ while caller.is_connected:
172
+ await asyncio.sleep(self.connection_poll)
173
+ await self._background_end("connection-drop")
174
+
175
+ async def _background_end(self, reason: str) -> None:
176
+ try:
177
+ await self._end(reason)
178
+ except Exception:
179
+ logger.exception("voice session %r failed to end cleanly", self.id)
180
+
181
+ async def _end(self, reason: str) -> None:
182
+ if self._ended:
183
+ return
184
+ self._ended = True
185
+ watcher, self._watcher = self._watcher, None
186
+ if watcher is not None and watcher is not asyncio.current_task():
187
+ watcher.cancel()
188
+ provider, self._provider = self._provider, None
189
+ if provider is not None:
190
+ try:
191
+ await provider.close()
192
+ except Exception:
193
+ logger.exception("voice provider close failed for session %r", self.id)
194
+ await self._flush()
195
+ turns, self._turns = self._turns, []
196
+ for turn in turns:
197
+ self._host.drop_ephemeral_message(turn["id"])
198
+ self.status.value = "closed"
199
+ self.delete()
200
+
201
+ # ─── Events sink ────────────────────────────────────────
202
+
203
+ def _turn(self, turn_id: str) -> dict[str, Any]:
204
+ for turn in self._turns:
205
+ if turn["id"] == turn_id:
206
+ return turn
207
+ raise ValueError(f"turn {turn_id!r} was never started")
208
+
209
+ @property
210
+ def _host(self) -> Any:
211
+ return self.handler.__self__
212
+
213
+ def _render(self, turn: dict[str, Any]) -> None:
214
+ self._host.upsert_ephemeral_message(
215
+ {"id": turn["id"], "role": turn["role"], "text": turn["text"]}
216
+ )
217
+
218
+ async def _flush(self) -> None:
219
+ # the lock serializes commits in done-order across turn-done and end
220
+ async with self._flushing:
221
+ done = [t for t in self._turns if t["done"]]
222
+ if not done:
223
+ return
224
+ try:
225
+ await self._host.commit_voice_turns(
226
+ [{"id": t["id"], "role": t["role"], "text": t["text"]} for t in done]
227
+ )
228
+ except Exception:
229
+ logger.exception("voice session %r turn commit failed", self.id)
230
+ return
231
+ committed = {t["id"] for t in done}
232
+ self._turns = [t for t in self._turns if t["id"] not in committed]
233
+ for turn_id in committed:
234
+ self._host.drop_ephemeral_message(turn_id)
235
+
236
+ def turn_started(self, turn_id: str, role: str) -> None:
237
+ if self._ended:
238
+ return
239
+ if any(turn["id"] == turn_id for turn in self._turns):
240
+ raise ValueError(f"turn {turn_id!r} already started")
241
+ turn = {"id": turn_id, "role": role, "text": "", "done": False}
242
+ self._turns.append(turn)
243
+ self._render(turn)
244
+
245
+ def turn_delta(self, turn_id: str, text: str) -> None:
246
+ if self._ended:
247
+ return
248
+ turn = self._turn(turn_id)
249
+ turn["text"] += text
250
+ self._render(turn)
251
+
252
+ def turn_done(self, turn_id: str, text: str) -> None:
253
+ if self._ended:
254
+ return
255
+ turn = self._turn(turn_id)
256
+ turn["text"] = text
257
+ turn["done"] = True
258
+ self._render(turn)
259
+ self._spawn(self._flush())
260
+
261
+ def tool_requested(self, request: "VoiceManager.ToolRequest") -> None:
262
+ logger.info("voice session %r tool request ignored: %r", self.id, request)
263
+
264
+ def rollover_approaching(self, rollover_id: str, deadline: float) -> None:
265
+ logger.info(
266
+ "voice session %r rollover %r ignored (deadline %s)",
267
+ self.id, rollover_id, deadline,
268
+ )
269
+
270
+ def closed(self, reason: str) -> None:
271
+ if self._ended:
272
+ return
273
+ self._spawn(self._background_end(reason))
274
+
275
+
276
+ class VoiceProvider(Protocol):
277
+ """The vendor seam: one live voice session's transport and control plane."""
278
+
279
+ async def start(
280
+ self, offer: str, *,
281
+ seed: "list[VoiceManager.SeedMessage]",
282
+ events: "VoiceManager.Events",
283
+ ) -> str:
284
+ """Open the session from an SDP offer and return the SDP answer; events flow into the sink."""
285
+ ...
286
+
287
+ async def append_context(
288
+ self, text: str, *, channel: Literal["speakable", "commentary", "developer"]
289
+ ) -> None: ...
290
+
291
+ async def return_tool_result(self, call_id: str, output: str) -> None: ...
292
+
293
+ async def select_rollover(
294
+ self, rollover_id: str, seed: "list[VoiceManager.SeedMessage]"
295
+ ) -> None: ...
296
+
297
+ async def close(self) -> None: ...
298
+
299
+
300
+ class VoiceManagerNamespace(TrayMapNamespace):
301
+ """The subsystem's command surface: at most one live session, its tray
302
+ mounted as ``state["voice"]``."""
303
+
304
+ def __init__(self, api: Any, spec: TrayDescriptor) -> None:
305
+ super().__init__(api, spec)
306
+ for name in (
307
+ "get_voice_seed",
308
+ "commit_voice_turns",
309
+ "upsert_ephemeral_message",
310
+ "drop_ephemeral_message",
311
+ ):
312
+ if not callable(getattr(api, name, None)):
313
+ raise TypeError(
314
+ f"{type(api).__name__} must implement {name} "
315
+ "(VoiceManager.Host) to host @voice_manager"
316
+ )
317
+ self._session: VoiceManager | None = None
318
+ self._starting = False
319
+ api.state["voice"] = None
320
+
321
+ def live(self) -> VoiceManager | None:
322
+ """The live session's tray; None at rest."""
323
+ session = self._session
324
+ return session if session is not None and not session._ended else None
325
+
326
+ def _mount(self, tray: VoiceManager) -> None:
327
+ if tray.storage._restored:
328
+ return # a crash's leftover row: the pump finishes its delete unmounted
329
+ self._session = tray
330
+ self._api.state["voice"] = tray.state
331
+
332
+ def _unmount(self, tray: VoiceManager) -> None:
333
+ if self._session is tray:
334
+ self._session = None
335
+ self._api.state["voice"] = None
336
+
337
+ async def start(self, params: Any, *, ctx: Any) -> Any:
338
+ if not isinstance(params, dict):
339
+ raise _reject("invalid-message", "params must be an object")
340
+ offer = params.get("offer")
341
+ if not isinstance(offer, str) or offer == "":
342
+ raise _reject("invalid-message", "offer must be a non-empty SDP string")
343
+ if self._starting or self.live() is not None:
344
+ raise _reject("voice-active", "a voice session is already live")
345
+ self._starting = True
346
+ try:
347
+ seed = _capped_seed(await self._api.get_voice_seed())
348
+ tray = await self.get_or_create(uuid4().hex)
349
+ assert isinstance(tray, VoiceManager)
350
+ return await tray._begin(offer, seed, ctx.caller, ctx.applied)
351
+ finally:
352
+ self._starting = False
353
+
354
+ async def end(self, *, ctx: Any) -> None:
355
+ session = self.live()
356
+ if session is None:
357
+ raise _reject("no-voice", "no voice session is live")
358
+ if session.clientId.value != ctx.caller.client_id:
359
+ raise _reject("not-voice-owner", "voice/end must come from the session owner")
360
+ await session._end("client")
361
+ ctx.applied()
362
+
363
+
364
+ class VoiceManagerDescriptor(TrayDescriptor):
365
+ namespace_cls = VoiceManagerNamespace
366
+
367
+ def __init__(self, handler: Any) -> None:
368
+ super().__init__(VoiceManager, handler, always_open=False)
369
+
370
+ async def mount(api: Any, id: str, tray: VoiceManager):
371
+ ns: VoiceManagerNamespace = getattr(api, self.name)
372
+ ns._mount(tray)
373
+ yield
374
+ ns._unmount(tray)
375
+
376
+ self._on_activate.append(mount)
377
+
378
+
379
+ def voice_manager() -> Callable[[Callable[..., Any]], VoiceManagerDescriptor]:
380
+ """Declare the voice subsystem on a Statewire host: the decorated method
381
+ is the per-session provider factory and ``self.<name>`` is the command
382
+ surface."""
383
+
384
+ def decorate(fn: Callable[..., Any]) -> VoiceManagerDescriptor:
385
+ if inspect.iscoroutinefunction(fn):
386
+ return VoiceManagerDescriptor(fn)
387
+
388
+ @functools.wraps(fn)
389
+ async def factory(self: Any, ctx: "VoiceManager.SessionContext") -> Any:
390
+ return fn(self, ctx)
391
+
392
+ return VoiceManagerDescriptor(factory)
393
+
394
+ return decorate
395
+
396
+
397
+ def _voice_of(host: Any) -> VoiceManagerNamespace | None:
398
+ for cls in type(host).__mro__:
399
+ for name, member in vars(cls).items():
400
+ if isinstance(member, VoiceManagerDescriptor):
401
+ return getattr(host, name)
402
+ return None
403
+
404
+
405
+ async def start_voice(host: Any, params: Any, *, ctx: Any) -> Any:
406
+ """The ``voice/start`` command body: routes to the host's declared voice manager."""
407
+ namespace = _voice_of(host)
408
+ if namespace is None:
409
+ raise _reject("no-voice", "this host declares no voice manager")
410
+ return await namespace.start(params, ctx=ctx)
411
+
412
+
413
+ async def end_voice(host: Any, *, ctx: Any) -> None:
414
+ """The ``voice/end`` command body: routes to the host's declared voice manager."""
415
+ namespace = _voice_of(host)
416
+ if namespace is None:
417
+ raise _reject("no-voice", "this host declares no voice manager")
418
+ return await namespace.end(ctx=ctx)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: harness-sdk-python
3
- Version: 0.26.0
3
+ Version: 0.28.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
@@ -0,0 +1,10 @@
1
+ harness_sdk/__init__.py,sha256=N9gOUKqe0_eM7cIA9sfA_lH1FBw1MN8U-zerwGAPVHE,1094
2
+ harness_sdk/checkpoints.py,sha256=L02l1IyLDj2b-GD7_HGrck7uenALeI4kg8yJSzoNVmc,25999
3
+ harness_sdk/deepagents.py,sha256=nnpJ65NeVXcpZNoFOSq6brVYrKdEqSjWgf6wz_WlHiw,12145
4
+ harness_sdk/fenced_postgres.py,sha256=hbbrMR1WaqrHTNgd4wFrXfRrzQFGnyim-c8nZXgwWp4,13235
5
+ harness_sdk/linear_thread.py,sha256=laDRXKLFsMlyWS4snqNAM6t38wO26gevLWXRNivhHSM,1810
6
+ harness_sdk/run_manager.py,sha256=CbY_4dpFeqGx3kJNI1ffsu3uIvzVc-Tc1UB6Nr9SiHY,77654
7
+ harness_sdk/voice.py,sha256=e9aw3hwgOq9l34VSyySxhfK3RY1WErPtZ_kVlBHtuHE,15663
8
+ harness_sdk_python-0.28.0.dist-info/METADATA,sha256=Vut2SI_mn0FFCWh8n_cLh4lJp_ICPIbkUgoxjPEg3pU,2200
9
+ harness_sdk_python-0.28.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
10
+ harness_sdk_python-0.28.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- harness_sdk/__init__.py,sha256=ehKf5P2F8tmWWGJDdxKB082dGV7GWlxXSo-GAhH3Ne4,827
2
- harness_sdk/checkpoints.py,sha256=L02l1IyLDj2b-GD7_HGrck7uenALeI4kg8yJSzoNVmc,25999
3
- harness_sdk/deepagents.py,sha256=Zl5n8Xu8LVQZEwmDnG1sfNzLhiNH5Miv1__bjZPb_bE,8741
4
- harness_sdk/fenced_postgres.py,sha256=hbbrMR1WaqrHTNgd4wFrXfRrzQFGnyim-c8nZXgwWp4,13235
5
- harness_sdk/linear_thread.py,sha256=laDRXKLFsMlyWS4snqNAM6t38wO26gevLWXRNivhHSM,1810
6
- harness_sdk/run_manager.py,sha256=CbY_4dpFeqGx3kJNI1ffsu3uIvzVc-Tc1UB6Nr9SiHY,77654
7
- harness_sdk_python-0.26.0.dist-info/METADATA,sha256=2nu-IellMnPO7WAGwWMEm2kYIPfqwFo3HXKQG6cs3kE,2200
8
- harness_sdk_python-0.26.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
- harness_sdk_python-0.26.0.dist-info/RECORD,,