harness-sdk-python 0.38.0__tar.gz → 0.39.1__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.38.0 → harness_sdk_python-0.39.1}/.gitignore +1 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/PKG-INFO +7 -4
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/README.md +4 -1
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/examples/runs_app.py +8 -2
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/pyproject.toml +2 -2
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/deepagents.py +75 -20
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/deepagents_host.py +17 -7
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/parts.py +13 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/protocol.py +1 -1
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/run_manager.py +117 -37
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/voice.py +78 -11
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/voice_thread.py +11 -2
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/wire.py +5 -5
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/run_helpers.py +37 -4
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_admit_steer.py +34 -3
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_branch_anchor.py +129 -0
- harness_sdk_python-0.39.1/tests/test_deepagents_approval.py +99 -0
- harness_sdk_python-0.39.1/tests/test_deepagents_files.py +123 -0
- harness_sdk_python-0.39.1/tests/test_deepagents_outcome.py +133 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_deepagents_thread.py +98 -4
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_deepagents_voice.py +10 -9
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_edit_reload.py +9 -7
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_enqueue.py +45 -0
- harness_sdk_python-0.39.1/tests/test_parts.py +96 -0
- harness_sdk_python-0.39.1/tests/test_run_id_bounds.py +267 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_steer.py +44 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_voice.py +71 -30
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_voice_delegation.py +22 -7
- harness_sdk_python-0.39.1/tests/test_voice_events.py +175 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_wire.py +25 -1
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/voice_helpers.py +4 -0
- harness_sdk_python-0.38.0/tests/test_parts.py +0 -49
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/examples/__init__.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/checkpoints.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/interest.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/langgraph.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/messages.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/providers/__init__.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/providers/gpt_live.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/src/harness_sdk/threads.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_checkpoints.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_gpt_live.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_imports.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_interest.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_langgraph.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_mint_concurrency.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_run_admission.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_run_eviction.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_run_ownership.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_run_sync.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_stopping_window.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_threads.py +0 -0
- {harness_sdk_python-0.38.0 → harness_sdk_python-0.39.1}/tests/test_undo.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: harness-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.39.1
|
|
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
|
|
7
7
|
Requires-Python: <4.0,>=3.12
|
|
8
|
-
Requires-Dist: pinned<0.
|
|
9
|
-
Requires-Dist: statewire<0.
|
|
8
|
+
Requires-Dist: pinned<0.17,>=0.16.0
|
|
9
|
+
Requires-Dist: statewire<0.23,>=0.21.2
|
|
10
10
|
Provides-Extra: deepagents
|
|
11
11
|
Requires-Dist: deepagents>=0.6.12; extra == 'deepagents'
|
|
12
12
|
Requires-Dist: langchain-core>=0.3; extra == 'deepagents'
|
|
@@ -57,7 +57,10 @@ class RunsHost(Statewire):
|
|
|
57
57
|
@command("run/enqueue")
|
|
58
58
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
59
59
|
return await self.run.enqueue(
|
|
60
|
-
params,
|
|
60
|
+
params,
|
|
61
|
+
applied=ctx.applied,
|
|
62
|
+
auth_result=auth_result,
|
|
63
|
+
version=ctx.version,
|
|
61
64
|
)
|
|
62
65
|
```
|
|
63
66
|
|
|
@@ -35,7 +35,10 @@ class RunsHost(Statewire):
|
|
|
35
35
|
@command("run/enqueue")
|
|
36
36
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
37
37
|
return await self.run.enqueue(
|
|
38
|
-
params,
|
|
38
|
+
params,
|
|
39
|
+
applied=ctx.applied,
|
|
40
|
+
auth_result=auth_result,
|
|
41
|
+
version=ctx.version,
|
|
39
42
|
)
|
|
40
43
|
```
|
|
41
44
|
|
|
@@ -158,13 +158,19 @@ class RunsHost(Statewire):
|
|
|
158
158
|
@command("run/enqueue")
|
|
159
159
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
160
160
|
return await self.run.enqueue(
|
|
161
|
-
params,
|
|
161
|
+
params,
|
|
162
|
+
applied=ctx.applied,
|
|
163
|
+
auth_result=auth_result,
|
|
164
|
+
version=ctx.version,
|
|
162
165
|
)
|
|
163
166
|
|
|
164
167
|
@command("run/steer")
|
|
165
168
|
async def run_steer(self, params, *, ctx, auth_result):
|
|
166
169
|
return await self.run.steer(
|
|
167
|
-
params,
|
|
170
|
+
params,
|
|
171
|
+
applied=ctx.applied,
|
|
172
|
+
auth_result=auth_result,
|
|
173
|
+
version=ctx.version,
|
|
168
174
|
)
|
|
169
175
|
|
|
170
176
|
@command("run/dequeue")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "harness-sdk-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.39.1"
|
|
4
4
|
description = "RunManager: the harness-sdk runs subsystem for Python Statewire hosts"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
7
7
|
requires-python = ">=3.12,<4.0"
|
|
8
|
-
dependencies = ["statewire>=0.
|
|
8
|
+
dependencies = ["statewire>=0.21.2,<0.23", "pinned>=0.16.0,<0.17"]
|
|
9
9
|
|
|
10
10
|
[project.optional-dependencies]
|
|
11
11
|
postgres = ["langgraph-checkpoint-postgres>=2.0.0"]
|
|
@@ -10,6 +10,7 @@ main run; its rows persist under ``checkpoint_ns == ns``.
|
|
|
10
10
|
from typing import Any, Callable
|
|
11
11
|
|
|
12
12
|
from statepatch import plain
|
|
13
|
+
from statewire import StatewireReject
|
|
13
14
|
|
|
14
15
|
from .checkpoints import (
|
|
15
16
|
CheckpointIndex,
|
|
@@ -32,8 +33,45 @@ def deepagents_role(message: dict[str, Any]) -> str:
|
|
|
32
33
|
raise ValueError(f"unsupported message type: {message['type']!r}") from exc
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
|
|
36
|
+
def _file_block(part: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
url, media_type = part["url"], part["mediaType"]
|
|
38
|
+
if url.startswith("data:"):
|
|
39
|
+
header, comma, data = url.removeprefix("data:").partition(",")
|
|
40
|
+
media_type, *params = header.split(";")
|
|
41
|
+
if not comma or not media_type or params[-1:] != ["base64"]:
|
|
42
|
+
raise ValueError("file data URLs must be data:<type>;base64,<data>")
|
|
43
|
+
source = {"base64": data, "mime_type": media_type}
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("file URLs must be inline data:<type>;base64,<data> URLs")
|
|
46
|
+
kind = media_type.split("/", 1)[0]
|
|
47
|
+
block = {"type": kind if kind in ("image", "audio", "video") else "file", **source}
|
|
48
|
+
if "filename" in part:
|
|
49
|
+
block["extras"] = {"filename": part["filename"]}
|
|
50
|
+
return block
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _user_content(message: dict[str, Any]) -> str | list[dict[str, Any]]:
|
|
54
|
+
parts = message["parts"]
|
|
55
|
+
if all(part["type"] == "text" for part in parts):
|
|
56
|
+
return "\n\n".join(part["text"] for part in parts)
|
|
57
|
+
return [{"type": "text", "text": part["text"]} if part["type"] == "text" else _file_block(part) for part in parts]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def check_file_parts(message: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
"""``prepare_message`` hook: a file part ``_file_block`` cannot carry rejects ``invalid-message``."""
|
|
62
|
+
parts = message.get("parts")
|
|
63
|
+
for part in parts if isinstance(parts, list) else ():
|
|
64
|
+
if (
|
|
65
|
+
isinstance(part, dict)
|
|
66
|
+
and part.get("type") == "file"
|
|
67
|
+
and isinstance(part.get("url"), str)
|
|
68
|
+
and isinstance(part.get("mediaType"), str)
|
|
69
|
+
):
|
|
70
|
+
try:
|
|
71
|
+
_file_block(part)
|
|
72
|
+
except ValueError as exc:
|
|
73
|
+
raise StatewireReject(str(exc), payload={"reason": "invalid-message"}) from exc
|
|
74
|
+
return message
|
|
37
75
|
|
|
38
76
|
|
|
39
77
|
def _ns_of(namespace: tuple[str, ...]) -> str:
|
|
@@ -123,19 +161,25 @@ class DeepagentsThread:
|
|
|
123
161
|
"""The dispatch's langgraph (payload, config): forks at the anchor when the batch does not extend the head."""
|
|
124
162
|
self.abandon_turn()
|
|
125
163
|
config = self.config()
|
|
126
|
-
if ctx.resume:
|
|
127
|
-
return None, config
|
|
128
164
|
thread = self.tree.thread(MAIN_NS)
|
|
129
165
|
if ctx.trigger == "input-resume":
|
|
130
166
|
from langgraph.types import Command
|
|
131
167
|
|
|
168
|
+
tool_ids = {p.get("toolInvocationId") for p in plain(self.tree.get(MAIN_NS, thread.head)["parts"])}
|
|
169
|
+
missing = [r["toolCallId"] for r, _response, _meta in ctx.input_outcomes if r["toolCallId"] not in tool_ids]
|
|
170
|
+
if missing:
|
|
171
|
+
raise KeyError(f"head {thread.head!r} holds no tool part for {missing!r}")
|
|
172
|
+
self._mount(MAIN_NS, thread.head)
|
|
173
|
+
if ctx.resume:
|
|
174
|
+
return None, config
|
|
132
175
|
decisions = [
|
|
133
176
|
{"type": "approve"} if response and response.get("decision") == "approve" else {"type": "reject"}
|
|
134
177
|
for _request, response, _meta in ctx.input_outcomes
|
|
135
178
|
]
|
|
136
179
|
self._admission = ctx.applied()
|
|
137
|
-
self._assemblers.pop(MAIN_NS, None)
|
|
138
180
|
return Command(resume={"decisions": decisions}), config
|
|
181
|
+
if ctx.resume:
|
|
182
|
+
return None, config
|
|
139
183
|
if ctx.has_rollback:
|
|
140
184
|
parent = ctx.rollback_to
|
|
141
185
|
elif ctx.messages:
|
|
@@ -156,7 +200,7 @@ class DeepagentsThread:
|
|
|
156
200
|
|
|
157
201
|
turn: list[Any] = [*plan.replay]
|
|
158
202
|
for message in ctx.messages:
|
|
159
|
-
turn.append(HumanMessage(id=message["id"], content=
|
|
203
|
+
turn.append(HumanMessage(id=message["id"], content=_user_content(message)))
|
|
160
204
|
self._append_user(message, parent)
|
|
161
205
|
parent = message["id"]
|
|
162
206
|
self.tree.set_head(MAIN_NS, parent)
|
|
@@ -196,7 +240,7 @@ class DeepagentsThread:
|
|
|
196
240
|
turn = []
|
|
197
241
|
for message in messages:
|
|
198
242
|
self._append_user(message, thread.head)
|
|
199
|
-
turn.append(HumanMessage(id=message["id"], content=
|
|
243
|
+
turn.append(HumanMessage(id=message["id"], content=_user_content(message)))
|
|
200
244
|
self._thread_state(MAIN_NS)
|
|
201
245
|
self._assemblers.pop(MAIN_NS, None)
|
|
202
246
|
self._changed()
|
|
@@ -233,10 +277,14 @@ class DeepagentsThread:
|
|
|
233
277
|
"metadata": {"provider": {"ids": []}},
|
|
234
278
|
}
|
|
235
279
|
self.tree.add(ns, value)
|
|
236
|
-
self.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
280
|
+
assembler = self._mount(ns, message_id)
|
|
281
|
+
return assembler
|
|
282
|
+
|
|
283
|
+
def _mount(self, ns: str, message_id: str) -> PartsAssembler:
|
|
284
|
+
self._touched.add((ns, message_id))
|
|
285
|
+
assembler = self._assemblers[ns] = PartsAssembler(lambda: self.tree.get(ns, message_id))
|
|
286
|
+
self._thread_state(ns, status="streaming")
|
|
287
|
+
self._changed()
|
|
240
288
|
return assembler
|
|
241
289
|
|
|
242
290
|
def _fold_id(self, assembler: PartsAssembler, langchain_id: str | None) -> None:
|
|
@@ -317,14 +365,16 @@ class DeepagentsThread:
|
|
|
317
365
|
self.interrupt = {"toolCallId": tool_id}
|
|
318
366
|
self._changed()
|
|
319
367
|
|
|
320
|
-
def _finish(self, ns: str) -> None:
|
|
368
|
+
def _finish(self, ns: str, outcome: dict[str, Any] | None = None) -> None:
|
|
321
369
|
assembler = self._assemblers.pop(ns, None)
|
|
322
370
|
if assembler is not None:
|
|
323
371
|
assembler.finish()
|
|
372
|
+
for key, value in (outcome or {}).items():
|
|
373
|
+
assembler.message[key] = value
|
|
324
374
|
self._thread_state(ns, status="idle")
|
|
325
375
|
|
|
326
|
-
async def ingest_turn(self, *, refetch: bool = False) -> None:
|
|
327
|
-
"""Ingest the turn's captured root checkpoint, persist the touched harness messages, and settle streaming state."""
|
|
376
|
+
async def ingest_turn(self, *, refetch: bool = False, outcome: dict[str, Any] | None = None) -> None:
|
|
377
|
+
"""Ingest the turn's captured root checkpoint, persist the touched harness messages, and settle streaming state; ``outcome`` lands on the turn's open replies."""
|
|
328
378
|
captured, self._captured = self._captured, None
|
|
329
379
|
if refetch:
|
|
330
380
|
await self._thread.ingest_latest()
|
|
@@ -336,7 +386,7 @@ class DeepagentsThread:
|
|
|
336
386
|
self._confirm_admission()
|
|
337
387
|
for ns in self._turn_namespaces():
|
|
338
388
|
if self.interrupt is None or ns != MAIN_NS:
|
|
339
|
-
self._finish(ns)
|
|
389
|
+
self._finish(ns, outcome)
|
|
340
390
|
for ns in self.tree.threads:
|
|
341
391
|
if ns != MAIN_NS and _root_of(ns) not in self._subagents:
|
|
342
392
|
self._thread_state(ns, status="idle")
|
|
@@ -354,14 +404,14 @@ class DeepagentsThread:
|
|
|
354
404
|
raise ValueError(f"subagent namespace must be a root namespace other than {MAIN_NS!r}, got {ns!r}")
|
|
355
405
|
if ns in self._subagents:
|
|
356
406
|
raise RuntimeError(f"subagent {ns!r} is already running")
|
|
407
|
+
config = {"configurable": {"thread_id": f"{self._thread_id}/{ns}"}}
|
|
408
|
+
payload = {"messages": [HumanMessage(id=message["id"], content=_user_content(message))]}
|
|
357
409
|
thread = self.tree.thread(ns)
|
|
358
410
|
self.tree.add(ns, {**message, "parentId": thread.head})
|
|
359
411
|
self._touched.add((ns, message["id"]))
|
|
360
412
|
self._thread_state(ns, status="streaming")
|
|
361
413
|
self._subagents.add(ns)
|
|
362
414
|
self._changed()
|
|
363
|
-
config = {"configurable": {"thread_id": f"{self._thread_id}/{ns}"}}
|
|
364
|
-
payload = {"messages": [HumanMessage(id=message["id"], content=_user_text(message))]}
|
|
365
415
|
try:
|
|
366
416
|
async for namespace, mode, event in self._agent.astream(
|
|
367
417
|
payload, config=config, stream_mode=["messages", "updates", "checkpoints"], subgraphs=True
|
|
@@ -416,14 +466,19 @@ class DeepagentsThread:
|
|
|
416
466
|
await self._persist()
|
|
417
467
|
self._changed()
|
|
418
468
|
|
|
419
|
-
def
|
|
420
|
-
"""
|
|
469
|
+
async def fail_turn(self, error: str) -> None:
|
|
470
|
+
"""Abandon the turn with its open replies marked ``failed`` and persist the touched messages."""
|
|
471
|
+
self.abandon_turn({"status": "failed", "error": error})
|
|
472
|
+
await self._persist()
|
|
473
|
+
|
|
474
|
+
def abandon_turn(self, outcome: dict[str, Any] | None = None) -> None:
|
|
475
|
+
"""Discard a stream capture and any fork planned for the abandoned turn; ``outcome`` lands on the turn's open replies."""
|
|
421
476
|
self._captured = None
|
|
422
477
|
self._admission = None
|
|
423
478
|
self._input_checkpoint_seen = False
|
|
424
479
|
self.interrupt = None
|
|
425
480
|
for ns in self._turn_namespaces():
|
|
426
|
-
self._finish(ns)
|
|
481
|
+
self._finish(ns, outcome)
|
|
427
482
|
self._thread.abandon_plan()
|
|
428
483
|
|
|
429
484
|
|
|
@@ -14,10 +14,10 @@ from typing import Any
|
|
|
14
14
|
from statewire import Statewire, command
|
|
15
15
|
|
|
16
16
|
from .checkpoints import CheckpointIndex
|
|
17
|
-
from .deepagents import DeepagentsThread
|
|
17
|
+
from .deepagents import DeepagentsThread, check_file_parts
|
|
18
18
|
from .messages import MAIN_NS
|
|
19
19
|
from .protocol import HARNESS_PROTOCOL
|
|
20
|
-
from .run_manager import RunManager, run_manager
|
|
20
|
+
from .run_manager import RunManager, error_of, run_manager
|
|
21
21
|
from .voice import VoiceTurnCommitDeferred, end_voice, set_voice_mic, start_voice, voice_client_departed
|
|
22
22
|
from .voice_thread import VoiceThread, is_ephemeral
|
|
23
23
|
from .wire import HarnessMessages
|
|
@@ -26,6 +26,11 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
_HALTED = ("stopped", "error")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _interrupted(ctx: RunManager.RunContext) -> dict[str, Any]:
|
|
30
|
+
reason = ctx.stop_reason
|
|
31
|
+
return {"status": "interrupted"} if reason is None else {"status": "interrupted", "stopReason": reason}
|
|
32
|
+
|
|
33
|
+
|
|
29
34
|
class DeepagentsHost(VoiceThread, HarnessMessages, Statewire):
|
|
30
35
|
"""One thread: ``build_agent(checkpointer)`` returns the compiled deepagents graph."""
|
|
31
36
|
|
|
@@ -128,15 +133,20 @@ class DeepagentsHost(VoiceThread, HarnessMessages, Statewire):
|
|
|
128
133
|
|
|
129
134
|
# ─── Runs ───────────────────────────────────────────────
|
|
130
135
|
|
|
131
|
-
@run_manager(capabilities=("rewind",))
|
|
136
|
+
@run_manager(capabilities=("rewind", "files"), prepare_message=check_file_parts)
|
|
132
137
|
async def run(self, ctx: RunManager.RunContext) -> Any:
|
|
133
138
|
payload, config = await self.messages.plan_turn(ctx)
|
|
134
139
|
while True:
|
|
135
140
|
try:
|
|
136
141
|
outcome = await self._stream_turn(ctx, payload, config)
|
|
137
|
-
await self.messages.ingest_turn(
|
|
142
|
+
await self.messages.ingest_turn(
|
|
143
|
+
refetch=outcome is not None, outcome=None if outcome is None else _interrupted(ctx)
|
|
144
|
+
)
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
await self.messages.fail_turn(error_of(exc)["message"])
|
|
147
|
+
raise
|
|
138
148
|
except BaseException:
|
|
139
|
-
self.messages.abandon_turn()
|
|
149
|
+
self.messages.abandon_turn(_interrupted(ctx))
|
|
140
150
|
raise
|
|
141
151
|
if outcome is not None:
|
|
142
152
|
return outcome
|
|
@@ -179,11 +189,11 @@ class DeepagentsHost(VoiceThread, HarnessMessages, Statewire):
|
|
|
179
189
|
|
|
180
190
|
@command("run/enqueue")
|
|
181
191
|
async def run_enqueue(self, params, *, ctx, auth_result):
|
|
182
|
-
return await self.run.enqueue(params, applied=ctx.applied, auth_result=auth_result)
|
|
192
|
+
return await self.run.enqueue(params, applied=ctx.applied, auth_result=auth_result, version=ctx.version)
|
|
183
193
|
|
|
184
194
|
@command("run/steer")
|
|
185
195
|
async def run_steer(self, params, *, ctx, auth_result):
|
|
186
|
-
return await self.run.steer(params, applied=ctx.applied, auth_result=auth_result)
|
|
196
|
+
return await self.run.steer(params, applied=ctx.applied, auth_result=auth_result, version=ctx.version)
|
|
187
197
|
|
|
188
198
|
@command("run/dequeue")
|
|
189
199
|
async def run_dequeue(self, params, *, auth_result):
|
|
@@ -73,6 +73,7 @@ class PartsAssembler:
|
|
|
73
73
|
text = text_of(content)
|
|
74
74
|
if text:
|
|
75
75
|
self._append_text("text", text, chunk_id)
|
|
76
|
+
self._cite(chunk)
|
|
76
77
|
for tool_chunk in getattr(chunk, "tool_call_chunks", None) or []:
|
|
77
78
|
key = (chunk_id, tool_chunk.get("index") or 0)
|
|
78
79
|
tool_id = tool_chunk.get("id") or self._chunk_tools.get(key)
|
|
@@ -120,6 +121,18 @@ class PartsAssembler:
|
|
|
120
121
|
elif call.get("args"):
|
|
121
122
|
self.parts[index]["input"] = call["args"]
|
|
122
123
|
|
|
124
|
+
def _cite(self, chunk: Any) -> None:
|
|
125
|
+
cited = {part["url"] for part in plain(self.parts) if part["type"] == "source"}
|
|
126
|
+
for block in chunk.content_blocks:
|
|
127
|
+
if block["type"] != "text":
|
|
128
|
+
continue
|
|
129
|
+
for note in block.get("annotations", []):
|
|
130
|
+
url = note.get("url") if note["type"] == "citation" else None
|
|
131
|
+
if url and url not in cited:
|
|
132
|
+
cited.add(url)
|
|
133
|
+
title = note.get("title")
|
|
134
|
+
self.parts.append({"type": "source", "url": url, **({"title": title} if title else {})})
|
|
135
|
+
|
|
123
136
|
def tool_result(self, message: Any) -> None:
|
|
124
137
|
tool_id = getattr(message, "tool_call_id", None)
|
|
125
138
|
index = self._index(tool_id) if tool_id else None
|