harborrag-engine 2.0.0a1__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.
- harborrag_engine/__init__.py +3 -0
- harborrag_engine/agent/__init__.py +37 -0
- harborrag_engine/agent/events.py +38 -0
- harborrag_engine/agent/execution.py +188 -0
- harborrag_engine/agent/guard.py +91 -0
- harborrag_engine/agent/helpers.py +68 -0
- harborrag_engine/agent/loop.py +343 -0
- harborrag_engine/agent/loop_state.py +54 -0
- harborrag_engine/agent/protocols.py +39 -0
- harborrag_engine/agent/run_lifecycle.py +126 -0
- harborrag_engine/agent/schemas.py +36 -0
- harborrag_engine/agent/service.py +186 -0
- harborrag_engine/agent/token_budget.py +55 -0
- harborrag_engine/agent/tool_execution.py +199 -0
- harborrag_engine/config.py +7 -0
- harborrag_engine/conversation/__init__.py +5 -0
- harborrag_engine/conversation/memory.py +9 -0
- harborrag_engine/ingestion/__init__.py +125 -0
- harborrag_engine/ingestion/admission.py +265 -0
- harborrag_engine/ingestion/base.py +35 -0
- harborrag_engine/ingestion/chunking/README.md +68 -0
- harborrag_engine/ingestion/chunking/__init__.py +90 -0
- harborrag_engine/ingestion/chunking/config.py +211 -0
- harborrag_engine/ingestion/chunking/errors.py +26 -0
- harborrag_engine/ingestion/chunking/identity/__init__.py +19 -0
- harborrag_engine/ingestion/chunking/identity/assignment.py +82 -0
- harborrag_engine/ingestion/chunking/identity/fingerprint.py +7 -0
- harborrag_engine/ingestion/chunking/pipeline/__init__.py +6 -0
- harborrag_engine/ingestion/chunking/pipeline/candidates.py +150 -0
- harborrag_engine/ingestion/chunking/pipeline/composition.py +64 -0
- harborrag_engine/ingestion/chunking/pipeline/result.py +149 -0
- harborrag_engine/ingestion/chunking/pipeline/service.py +31 -0
- harborrag_engine/ingestion/chunking/records/__init__.py +16 -0
- harborrag_engine/ingestion/chunking/records/context.py +131 -0
- harborrag_engine/ingestion/chunking/records/factory.py +261 -0
- harborrag_engine/ingestion/chunking/records/hierarchy.py +61 -0
- harborrag_engine/ingestion/chunking/records/rebind.py +41 -0
- harborrag_engine/ingestion/chunking/records/validation.py +217 -0
- harborrag_engine/ingestion/chunking/schemas.py +301 -0
- harborrag_engine/ingestion/chunking/sources/__init__.py +13 -0
- harborrag_engine/ingestion/chunking/sources/base.py +25 -0
- harborrag_engine/ingestion/chunking/sources/canonical.py +31 -0
- harborrag_engine/ingestion/chunking/sources/confluence.py +70 -0
- harborrag_engine/ingestion/chunking/sources/jira.py +338 -0
- harborrag_engine/ingestion/chunking/sources/registry.py +37 -0
- harborrag_engine/ingestion/chunking/sources/validation.py +29 -0
- harborrag_engine/ingestion/chunking/table/README.md +16 -0
- harborrag_engine/ingestion/chunking/table/__init__.py +1 -0
- harborrag_engine/ingestion/chunking/table/classifier.py +203 -0
- harborrag_engine/ingestion/chunking/table/errors.py +13 -0
- harborrag_engine/ingestion/chunking/table/factory.py +299 -0
- harborrag_engine/ingestion/chunking/table/fragmentation.py +68 -0
- harborrag_engine/ingestion/chunking/table/grouping.py +93 -0
- harborrag_engine/ingestion/chunking/table/models.py +132 -0
- harborrag_engine/ingestion/chunking/table/planner.py +277 -0
- harborrag_engine/ingestion/chunking/table/policy.py +96 -0
- harborrag_engine/ingestion/chunking/table/rendering.py +172 -0
- harborrag_engine/ingestion/chunking/table/service.py +141 -0
- harborrag_engine/ingestion/chunking/table/validator.py +174 -0
- harborrag_engine/ingestion/chunking/transforms/__init__.py +15 -0
- harborrag_engine/ingestion/chunking/transforms/packing.py +223 -0
- harborrag_engine/ingestion/chunking/transforms/refinement.py +253 -0
- harborrag_engine/ingestion/chunking/transforms/routing.py +195 -0
- harborrag_engine/ingestion/chunking/transforms/segmentation.py +274 -0
- harborrag_engine/ingestion/normalizer.py +217 -0
- harborrag_engine/ingestion/policies.py +210 -0
- harborrag_engine/ingestion/projections/__init__.py +43 -0
- harborrag_engine/ingestion/projections/graph/__init__.py +37 -0
- harborrag_engine/ingestion/projections/graph/collaboration_source_projectors.py +284 -0
- harborrag_engine/ingestion/projections/graph/file_source_projectors.py +197 -0
- harborrag_engine/ingestion/projections/graph/graph.py +209 -0
- harborrag_engine/ingestion/projections/graph/graph_models.py +179 -0
- harborrag_engine/ingestion/projections/graph/graph_state.py +292 -0
- harborrag_engine/ingestion/projections/graph/graph_structure.py +248 -0
- harborrag_engine/ingestion/projections/graph/source_projector_support.py +217 -0
- harborrag_engine/ingestion/projections/graph/source_projectors.py +96 -0
- harborrag_engine/ingestion/projections/vector/__init__.py +18 -0
- harborrag_engine/ingestion/projections/vector/vector.py +97 -0
- harborrag_engine/ingestion/projections/vector/vector_store.py +260 -0
- harborrag_engine/ingestion/projections/verification.py +222 -0
- harborrag_engine/ingestion/representations/__init__.py +10 -0
- harborrag_engine/ingestion/representations/encoding.py +142 -0
- harborrag_engine/ingestion/representations/reuse.py +160 -0
- harborrag_engine/ingestion/representations/sparse.py +80 -0
- harborrag_engine/ingestion/tables.py +222 -0
- harborrag_engine/ingestion/title_content.py +64 -0
- harborrag_engine/policy.py +11 -0
- harborrag_engine/retrieval/__init__.py +42 -0
- harborrag_engine/retrieval/active_versions.py +76 -0
- harborrag_engine/retrieval/authoritative.py +212 -0
- harborrag_engine/retrieval/evidence.py +53 -0
- harborrag_engine/retrieval/fusion.py +29 -0
- harborrag_engine/retrieval/graph.py +275 -0
- harborrag_engine/retrieval/pipeline.py +151 -0
- harborrag_engine/retrieval/ports.py +52 -0
- harborrag_engine/retrieval/reranking.py +5 -0
- harborrag_engine/retrieval/rewriting.py +2 -0
- harborrag_engine/testing/edge_signatures.py +33 -0
- harborrag_engine-2.0.0a1.dist-info/METADATA +121 -0
- harborrag_engine-2.0.0a1.dist-info/RECORD +102 -0
- harborrag_engine-2.0.0a1.dist-info/WHEEL +4 -0
- harborrag_engine-2.0.0a1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Provider-neutral agent orchestration."""
|
|
2
|
+
|
|
3
|
+
from harborrag_core.ports.agent_runs import (
|
|
4
|
+
AgentCheckpoint,
|
|
5
|
+
AgentRunIdentity,
|
|
6
|
+
AgentRunRepository,
|
|
7
|
+
AgentRunStatus,
|
|
8
|
+
AgentStopReason,
|
|
9
|
+
AgentToolExecution,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .events import AgentEvent, AgentEventSink
|
|
13
|
+
from .guard import ExecutionGuard
|
|
14
|
+
from .service import (
|
|
15
|
+
AgentRunOptions,
|
|
16
|
+
AgentRunResult,
|
|
17
|
+
AgentService,
|
|
18
|
+
AgentToolProvider,
|
|
19
|
+
AgentToolSpec,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AgentCheckpoint",
|
|
24
|
+
"AgentEvent",
|
|
25
|
+
"AgentEventSink",
|
|
26
|
+
"AgentRunIdentity",
|
|
27
|
+
"AgentRunOptions",
|
|
28
|
+
"AgentRunRepository",
|
|
29
|
+
"AgentRunResult",
|
|
30
|
+
"AgentRunStatus",
|
|
31
|
+
"AgentService",
|
|
32
|
+
"AgentStopReason",
|
|
33
|
+
"AgentToolExecution",
|
|
34
|
+
"AgentToolProvider",
|
|
35
|
+
"AgentToolSpec",
|
|
36
|
+
"ExecutionGuard",
|
|
37
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Transport-neutral progress events emitted by the agent loop.
|
|
2
|
+
|
|
3
|
+
The agent loop knows nothing about SSE, WebSockets, or logging -- it emits
|
|
4
|
+
``AgentEvent`` to an injected sink, and a transport layer decides how to
|
|
5
|
+
render each one. Passing no sink (the default) costs nothing: ``run()``
|
|
6
|
+
and ``resume()`` skip emission entirely when ``events`` is ``None``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class AgentEvent:
|
|
17
|
+
"""One point-in-time occurrence during an agent run.
|
|
18
|
+
|
|
19
|
+
``kind`` is a stable dotted name (``"run.started"``, ``"tool.completed"``,
|
|
20
|
+
...) a transport can use as a discriminator; ``data`` carries only
|
|
21
|
+
JSON-serializable, already-public fields -- the same values that appear
|
|
22
|
+
in ``AgentRunResult``/``AgentToolExecution``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
kind: str
|
|
26
|
+
run_id: str
|
|
27
|
+
data: Mapping[str, object]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
AgentEventSink = Callable[[AgentEvent], Awaitable[None]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def emit(sink: AgentEventSink | None, event: AgentEvent) -> None:
|
|
34
|
+
if sink is not None:
|
|
35
|
+
await sink(event)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["AgentEvent", "AgentEventSink", "emit"]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Model-turn and tool-call execution primitives used by the agent loop.
|
|
2
|
+
|
|
3
|
+
Split out of ``loop.py`` so that module stays focused on step orchestration
|
|
4
|
+
(guard checks, checkpointing, stop-reason bookkeeping) while this one owns
|
|
5
|
+
the actual calls out to the chat model and the tool provider.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from harborrag_core.models.chat import (
|
|
14
|
+
HarborChatMessage,
|
|
15
|
+
HarborChatMetadata,
|
|
16
|
+
HarborChatRequest,
|
|
17
|
+
HarborChatResponse,
|
|
18
|
+
HarborChatTool,
|
|
19
|
+
HarborToolCall,
|
|
20
|
+
)
|
|
21
|
+
from harborrag_core.ports.agent_runs import AgentToolExecution
|
|
22
|
+
from harborrag_engine.conversation import ConversationIdentity, ConversationMemory, ConversationTurn
|
|
23
|
+
|
|
24
|
+
from .guard import ExecutionGuard, digest_arguments
|
|
25
|
+
from .protocols import AgentChatModel, AgentToolProvider, AgentToolSpec
|
|
26
|
+
from .schemas import AgentRunOptions
|
|
27
|
+
from .tool_execution import VECTOR_SEARCH_TOOL, bounded_tool_result_content
|
|
28
|
+
|
|
29
|
+
_BLOCKED_TOOL_NAMES = frozenset({"agent", "chat"})
|
|
30
|
+
_GRAPH_TOOL_PREFIX = "graph_"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ChatAndToolExecutor:
|
|
34
|
+
"""Bind one model turn or one batch of tool calls to their transports."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
chat: AgentChatModel,
|
|
39
|
+
tools: AgentToolProvider,
|
|
40
|
+
*,
|
|
41
|
+
memory: ConversationMemory | None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._chat = chat
|
|
44
|
+
self._tools = tools
|
|
45
|
+
self._memory = memory
|
|
46
|
+
|
|
47
|
+
def available_specs(
|
|
48
|
+
self,
|
|
49
|
+
tenant_id: str,
|
|
50
|
+
*,
|
|
51
|
+
graph_search: bool,
|
|
52
|
+
) -> list[AgentToolSpec]:
|
|
53
|
+
return [
|
|
54
|
+
spec
|
|
55
|
+
for spec in self._tools.list_tools(tenant_id)
|
|
56
|
+
if spec.capability == "read"
|
|
57
|
+
and spec.name not in _BLOCKED_TOOL_NAMES
|
|
58
|
+
and (graph_search or not spec.name.startswith(_GRAPH_TOOL_PREFIX))
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
async def complete(
|
|
62
|
+
self,
|
|
63
|
+
messages: Sequence[HarborChatMessage],
|
|
64
|
+
options: AgentRunOptions,
|
|
65
|
+
*,
|
|
66
|
+
tools: tuple[HarborChatTool, ...],
|
|
67
|
+
guard: ExecutionGuard | None = None,
|
|
68
|
+
completion_token_limit: int | None = None,
|
|
69
|
+
) -> HarborChatResponse:
|
|
70
|
+
request = HarborChatRequest(
|
|
71
|
+
messages=tuple(messages),
|
|
72
|
+
tools=tools,
|
|
73
|
+
parallel_tool_calls=True if tools else None,
|
|
74
|
+
max_completion_tokens=completion_token_limit,
|
|
75
|
+
metadata=HarborChatMetadata(
|
|
76
|
+
tenant_id=options.tenant_id,
|
|
77
|
+
conversation_id=options.session_id,
|
|
78
|
+
),
|
|
79
|
+
sensitive=True,
|
|
80
|
+
)
|
|
81
|
+
coro = self._chat.complete(request)
|
|
82
|
+
remaining = guard.remaining_seconds() if guard is not None else None
|
|
83
|
+
if remaining is None:
|
|
84
|
+
return await coro
|
|
85
|
+
return await asyncio.wait_for(coro, timeout=remaining)
|
|
86
|
+
|
|
87
|
+
async def execute_tool_calls(
|
|
88
|
+
self,
|
|
89
|
+
calls: Sequence[HarborToolCall],
|
|
90
|
+
*,
|
|
91
|
+
step: int,
|
|
92
|
+
options: AgentRunOptions,
|
|
93
|
+
allowed_names: set[str],
|
|
94
|
+
guard: ExecutionGuard,
|
|
95
|
+
) -> tuple[tuple[HarborChatMessage, AgentToolExecution], ...]:
|
|
96
|
+
coro = asyncio.gather(
|
|
97
|
+
*(
|
|
98
|
+
self._execute(call, step=step, options=options, allowed_names=allowed_names)
|
|
99
|
+
for call in calls
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
remaining = guard.remaining_seconds()
|
|
103
|
+
if remaining is None:
|
|
104
|
+
return tuple(await coro)
|
|
105
|
+
return tuple(await asyncio.wait_for(coro, timeout=remaining))
|
|
106
|
+
|
|
107
|
+
async def remember(
|
|
108
|
+
self,
|
|
109
|
+
identity: ConversationIdentity | None,
|
|
110
|
+
current_user_message: HarborChatMessage | None,
|
|
111
|
+
response_text: str,
|
|
112
|
+
) -> None:
|
|
113
|
+
if (
|
|
114
|
+
self._memory is not None
|
|
115
|
+
and identity is not None
|
|
116
|
+
and current_user_message is not None
|
|
117
|
+
and isinstance(current_user_message.content, str)
|
|
118
|
+
):
|
|
119
|
+
await self._memory.append(
|
|
120
|
+
identity,
|
|
121
|
+
ConversationTurn(current_user_message.content, response_text),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def _execute(
|
|
125
|
+
self,
|
|
126
|
+
call: HarborToolCall,
|
|
127
|
+
*,
|
|
128
|
+
step: int,
|
|
129
|
+
options: AgentRunOptions,
|
|
130
|
+
allowed_names: set[str],
|
|
131
|
+
) -> tuple[HarborChatMessage, AgentToolExecution]:
|
|
132
|
+
name = call.function.name
|
|
133
|
+
arguments = call.function.parsed_arguments
|
|
134
|
+
digest = digest_arguments(
|
|
135
|
+
arguments if isinstance(arguments, dict) else {"__unparsed__": call.function.arguments}
|
|
136
|
+
)
|
|
137
|
+
result = await self._invoke(name, arguments, options=options, allowed_names=allowed_names)
|
|
138
|
+
content = bounded_tool_result_content(result)
|
|
139
|
+
return (
|
|
140
|
+
HarborChatMessage.tool(content, tool_call_id=call.id, name=name),
|
|
141
|
+
AgentToolExecution(
|
|
142
|
+
step=step,
|
|
143
|
+
call_id=call.id,
|
|
144
|
+
tool=name,
|
|
145
|
+
ok=result.get("ok") is True,
|
|
146
|
+
arguments_digest=digest,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def _invoke(
|
|
151
|
+
self,
|
|
152
|
+
name: str,
|
|
153
|
+
arguments: object,
|
|
154
|
+
*,
|
|
155
|
+
options: AgentRunOptions,
|
|
156
|
+
allowed_names: set[str],
|
|
157
|
+
) -> dict[str, object]:
|
|
158
|
+
"""Run one tool call, or explain why it can't run, without ever raising."""
|
|
159
|
+
|
|
160
|
+
if not isinstance(arguments, dict):
|
|
161
|
+
return {"ok": False, "error": "invalid tool arguments"}
|
|
162
|
+
if name not in allowed_names:
|
|
163
|
+
return {"ok": False, "error": "tool is not available to this agent"}
|
|
164
|
+
try:
|
|
165
|
+
return await self._tools.call_tool(
|
|
166
|
+
name,
|
|
167
|
+
self._scoped_arguments(name, arguments, options),
|
|
168
|
+
principal_id=options.principal_id,
|
|
169
|
+
)
|
|
170
|
+
except Exception: # noqa: BLE001 - tool failures become model-visible data
|
|
171
|
+
return {"ok": False, "error": "tool call failed"}
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _scoped_arguments(
|
|
175
|
+
name: str,
|
|
176
|
+
arguments: dict[str, object],
|
|
177
|
+
options: AgentRunOptions,
|
|
178
|
+
) -> dict[str, object]:
|
|
179
|
+
"""Bind a tool call to its caller's tenant, never trusting the model for it."""
|
|
180
|
+
|
|
181
|
+
scoped = dict(arguments)
|
|
182
|
+
scoped["tenant_id"] = options.tenant_id
|
|
183
|
+
if name == VECTOR_SEARCH_TOOL and not options.graph_search:
|
|
184
|
+
scoped["observe_graph"] = False
|
|
185
|
+
return scoped
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = ["ChatAndToolExecutor"]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Runaway-loop protection for the bounded agent loop.
|
|
2
|
+
|
|
3
|
+
``AgentRunOptions.max_steps`` alone does not stop a pathological run: a model
|
|
4
|
+
can spend its entire step budget calling the same tool with the same
|
|
5
|
+
arguments, or one step can hang indefinitely on a slow tool. ``ExecutionGuard``
|
|
6
|
+
adds the two checks a step-count bound cannot express: a wall-clock deadline
|
|
7
|
+
and repeated-identical-tool-call detection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from json import dumps
|
|
17
|
+
|
|
18
|
+
from harborrag_core.ports.agent_runs import AgentToolExecution
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def digest_arguments(arguments: dict[str, object]) -> str:
|
|
22
|
+
"""Stable hash of tool-call arguments, used as a repeat-detection key.
|
|
23
|
+
|
|
24
|
+
Only the digest is ever persisted or compared -- never the raw
|
|
25
|
+
arguments -- so this is safe to store in a public execution trace.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
canonical = dumps(arguments, sort_keys=True, separators=(",", ":"), default=str)
|
|
29
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class ExecutionGuard:
|
|
34
|
+
"""Bound wall-clock time and repeated tool calls within one run attempt.
|
|
35
|
+
|
|
36
|
+
A fresh guard is armed for each call to ``run()`` or ``resume()`` --
|
|
37
|
+
``timeout_seconds`` is a budget for *this* execution attempt, not the
|
|
38
|
+
run's total lifetime, since a resumed run may follow an arbitrarily long
|
|
39
|
+
gap after a crash.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
timeout_seconds: float | None = None
|
|
43
|
+
max_repeated_tool_calls: int = 2
|
|
44
|
+
_deadline: float | None = field(default=None, init=False, repr=False)
|
|
45
|
+
_call_counts: dict[tuple[str, str], int] = field(
|
|
46
|
+
default_factory=dict,
|
|
47
|
+
init=False,
|
|
48
|
+
repr=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.timeout_seconds is not None and self.timeout_seconds <= 0:
|
|
53
|
+
raise ValueError("agent timeout_seconds must be positive")
|
|
54
|
+
if self.max_repeated_tool_calls < 1:
|
|
55
|
+
raise ValueError("agent max_repeated_tool_calls must be at least 1")
|
|
56
|
+
|
|
57
|
+
def start(self) -> None:
|
|
58
|
+
"""Arm the deadline. Call exactly once at the start of an execution attempt."""
|
|
59
|
+
|
|
60
|
+
self._deadline = (
|
|
61
|
+
None if self.timeout_seconds is None else time.monotonic() + self.timeout_seconds
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def remaining_seconds(self) -> float | None:
|
|
65
|
+
"""Seconds left before the deadline, or ``None`` when unbounded."""
|
|
66
|
+
|
|
67
|
+
if self._deadline is None:
|
|
68
|
+
return None
|
|
69
|
+
return max(0.0, self._deadline - time.monotonic())
|
|
70
|
+
|
|
71
|
+
def timed_out(self) -> bool:
|
|
72
|
+
remaining = self.remaining_seconds()
|
|
73
|
+
return remaining is not None and remaining <= 0
|
|
74
|
+
|
|
75
|
+
def observe_tool_call(self, tool: str, arguments_digest: str) -> bool:
|
|
76
|
+
"""Record one tool call; return ``True`` once its repeat limit is exceeded."""
|
|
77
|
+
|
|
78
|
+
key = (tool, arguments_digest)
|
|
79
|
+
count = self._call_counts.get(key, 0) + 1
|
|
80
|
+
self._call_counts[key] = count
|
|
81
|
+
return count > self.max_repeated_tool_calls
|
|
82
|
+
|
|
83
|
+
def replay(self, executions: Iterable[AgentToolExecution]) -> None:
|
|
84
|
+
"""Rebuild repeated-call state from a checkpoint's executions on resume."""
|
|
85
|
+
|
|
86
|
+
for execution in executions:
|
|
87
|
+
key = (execution.tool, execution.arguments_digest)
|
|
88
|
+
self._call_counts[key] = self._call_counts.get(key, 0) + 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = ["ExecutionGuard", "digest_arguments"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Small pure helpers shared across the agent loop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from harborrag_core.models.chat import HarborChatMessage, HarborChatUsage, MessageRole
|
|
8
|
+
from harborrag_engine.conversation import ConversationTurn
|
|
9
|
+
|
|
10
|
+
from .schemas import AgentRunOptions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_options(options: AgentRunOptions) -> None:
|
|
14
|
+
for field_name, value in (
|
|
15
|
+
("tenant_id", options.tenant_id),
|
|
16
|
+
("principal_id", options.principal_id),
|
|
17
|
+
("session_id", options.session_id),
|
|
18
|
+
):
|
|
19
|
+
if not value.strip() or len(value) > 128:
|
|
20
|
+
raise ValueError(f"agent {field_name} must contain 1 to 128 non-whitespace characters")
|
|
21
|
+
if not 1 <= options.max_steps <= 8:
|
|
22
|
+
raise ValueError("agent max_steps must be between 1 and 8")
|
|
23
|
+
if options.timeout_seconds is not None and options.timeout_seconds <= 0:
|
|
24
|
+
raise ValueError("agent timeout_seconds must be positive")
|
|
25
|
+
if options.max_repeated_tool_calls < 1:
|
|
26
|
+
raise ValueError("agent max_repeated_tool_calls must be at least 1")
|
|
27
|
+
if options.synthesis_timeout_seconds is not None and options.synthesis_timeout_seconds <= 0:
|
|
28
|
+
raise ValueError("agent synthesis_timeout_seconds must be positive")
|
|
29
|
+
if options.max_total_tokens is not None and options.max_total_tokens < 1:
|
|
30
|
+
raise ValueError("agent max_total_tokens must be at least 1")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_usage(left: HarborChatUsage, right: HarborChatUsage) -> HarborChatUsage:
|
|
34
|
+
def optional_sum(name: str) -> int | None:
|
|
35
|
+
first = getattr(left, name)
|
|
36
|
+
second = getattr(right, name)
|
|
37
|
+
return None if first is None and second is None else (first or 0) + (second or 0)
|
|
38
|
+
|
|
39
|
+
return HarborChatUsage(
|
|
40
|
+
prompt_tokens=left.prompt_tokens + right.prompt_tokens,
|
|
41
|
+
completion_tokens=left.completion_tokens + right.completion_tokens,
|
|
42
|
+
total_tokens=left.total_tokens + right.total_tokens,
|
|
43
|
+
cache_read_input_tokens=optional_sum("cache_read_input_tokens"),
|
|
44
|
+
cache_creation_input_tokens=optional_sum("cache_creation_input_tokens"),
|
|
45
|
+
reasoning_tokens=optional_sum("reasoning_tokens"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def turn_messages(turns: Sequence[ConversationTurn]) -> list[HarborChatMessage]:
|
|
50
|
+
messages: list[HarborChatMessage] = []
|
|
51
|
+
for turn in turns:
|
|
52
|
+
messages.extend(
|
|
53
|
+
(
|
|
54
|
+
HarborChatMessage.user(turn.user_content),
|
|
55
|
+
HarborChatMessage.assistant(turn.assistant_content),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return messages
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def last_user_message(messages: Sequence[HarborChatMessage]) -> HarborChatMessage | None:
|
|
62
|
+
return next(
|
|
63
|
+
(message for message in reversed(messages) if message.role is MessageRole.USER),
|
|
64
|
+
None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["add_usage", "last_user_message", "turn_messages", "validate_options"]
|