pi-durable-python 1.3.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.
- pi_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
pi_durable/errors.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Public errors of the durable kernel and harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .ids import ConversationId
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def error_message(error: object) -> str:
|
|
11
|
+
"""Return a message for any thrown value without throwing while formatting it."""
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
return str(error)
|
|
15
|
+
except Exception:
|
|
16
|
+
return object.__repr__(error)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_failure(error: BaseException) -> bool:
|
|
20
|
+
"""Whether a thrown value is a failure of the work, rather than its teardown.
|
|
21
|
+
|
|
22
|
+
Section 10.1 ends the Session on what the work throws. Cancellation and the interpreter's
|
|
23
|
+
teardown signals (``CancelledError``, ``GeneratorExit``, ``SystemExit``, ``KeyboardInterrupt``)
|
|
24
|
+
derive from ``BaseException`` alone: they unwind the work that is running and must keep
|
|
25
|
+
unwinding, so they never end the Session and never become a task's fault.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
return isinstance(error, Exception)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _NamedError(Exception):
|
|
32
|
+
"""An error whose ``name`` matches its class, mirroring JavaScript ``Error.name``."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str) -> None:
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.name = type(self).__name__
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReadAfterWrite(_NamedError):
|
|
40
|
+
"""A transaction read a table after its first table write.
|
|
41
|
+
|
|
42
|
+
Read every required row before writing.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, method: str) -> None:
|
|
46
|
+
super().__init__(f"Tx.{method}() cannot read tables after the first table write")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StorageRequestError(_NamedError):
|
|
50
|
+
"""A Storage read rejected an invalid request, such as an unknown conversation or a foreign cursor.
|
|
51
|
+
|
|
52
|
+
It has no durable effect. Unlike any other error a Storage throws, it fails only that read,
|
|
53
|
+
not the Session; from ``commit()`` it is fatal like any other error.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, message: str) -> None:
|
|
57
|
+
super().__init__(message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SessionFailed(_NamedError):
|
|
61
|
+
"""The Session failed, so nothing runs or commits any more.
|
|
62
|
+
|
|
63
|
+
It fails when a Storage method throws, a commit cannot be adopted, or the Harness's own
|
|
64
|
+
bookkeeping throws. ``cause`` is that first error. Reopen the Session; reopening recovers from
|
|
65
|
+
what was committed.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, cause: object) -> None:
|
|
69
|
+
super().__init__("Session failed after a storage error; close and reopen it")
|
|
70
|
+
self.cause: Any = cause
|
|
71
|
+
if isinstance(cause, BaseException):
|
|
72
|
+
self.__cause__ = cause
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ConversationBusy(_NamedError):
|
|
76
|
+
"""A submission reached a busy conversation and was not admitted."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, conversation_id: ConversationId) -> None:
|
|
79
|
+
super().__init__(f"Conversation {conversation_id} is busy")
|
|
80
|
+
self.conversation_id = conversation_id
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"ConversationBusy",
|
|
85
|
+
"ReadAfterWrite",
|
|
86
|
+
"SessionFailed",
|
|
87
|
+
"StorageRequestError",
|
|
88
|
+
"error_message",
|
|
89
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Durable agent harness."""
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Agent state, settings, and registry-backed agent resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
from ..documents import define_doc
|
|
9
|
+
from ..session.transaction import Transaction
|
|
10
|
+
from ..types import ConversationId, ConversationRecord
|
|
11
|
+
from .types import (
|
|
12
|
+
Agent,
|
|
13
|
+
Extension,
|
|
14
|
+
PromptSection,
|
|
15
|
+
RegistrySnapshot,
|
|
16
|
+
Settings,
|
|
17
|
+
ToolRegistration,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
INSTRUCTIONS_KEY = "instructions"
|
|
21
|
+
|
|
22
|
+
DEFAULT_RETRY_POLICY: dict[str, Any] = {
|
|
23
|
+
"enabled": True,
|
|
24
|
+
"max_retries": 3,
|
|
25
|
+
"base_delay_ms": 2_000,
|
|
26
|
+
"max_agent_delay_ms": 60_000,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
DEFAULT_COMPACTION_POLICY: dict[str, Any] = {
|
|
30
|
+
"enabled": True,
|
|
31
|
+
"reserve_tokens": 16_384,
|
|
32
|
+
"keep_recent_tokens": 20_000,
|
|
33
|
+
"background_tokens": 32_768,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
DEFAULT_PROGRESS_POLICY: dict[str, Any] = {
|
|
37
|
+
"partial_interval_ms": 100,
|
|
38
|
+
"output_interval_ms": 100,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DEFAULT_CONTEXT_RETENTION_MS = 600_000
|
|
42
|
+
|
|
43
|
+
AgentDoc = define_doc(
|
|
44
|
+
{
|
|
45
|
+
"kind": "pi.agent",
|
|
46
|
+
"version": 1,
|
|
47
|
+
"scope": "conversation",
|
|
48
|
+
"history": "rewindable",
|
|
49
|
+
"fork": "asOf",
|
|
50
|
+
"initial": lambda: {},
|
|
51
|
+
"checkpoint_when": lambda _state, _ops, _info: True,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _field(value: Any, name: str, default: Any = None) -> Any:
|
|
57
|
+
if isinstance(value, Mapping):
|
|
58
|
+
return value.get(name, default)
|
|
59
|
+
return getattr(value, name, default)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_settings(settings: Any) -> Settings:
|
|
63
|
+
"""Resolve the host settings: every field over its built-in default, object fields merged."""
|
|
64
|
+
|
|
65
|
+
extensions = _field(settings, "extensions")
|
|
66
|
+
merged_retry = dict(DEFAULT_RETRY_POLICY)
|
|
67
|
+
merged_retry.update(_field(settings, "retry", {}) or {})
|
|
68
|
+
merged_compaction = dict(DEFAULT_COMPACTION_POLICY)
|
|
69
|
+
merged_compaction.update(_field(settings, "compaction", {}) or {})
|
|
70
|
+
partial = _field(settings, "progress", {}) or {}
|
|
71
|
+
progress = dict(DEFAULT_PROGRESS_POLICY)
|
|
72
|
+
for key in progress:
|
|
73
|
+
value = _field(partial, key)
|
|
74
|
+
if value is not None:
|
|
75
|
+
progress[key] = value
|
|
76
|
+
return Settings(
|
|
77
|
+
extensions=extensions,
|
|
78
|
+
stream=dict(_field(settings, "stream", {}) or {}),
|
|
79
|
+
retry=merged_retry,
|
|
80
|
+
compaction=merged_compaction,
|
|
81
|
+
progress=progress,
|
|
82
|
+
tool_execution=_field(settings, "tool_execution", "parallel"),
|
|
83
|
+
steering_mode=_field(settings, "steering_mode", "one-at-a-time"),
|
|
84
|
+
follow_up_mode=_field(settings, "follow_up_mode", "one-at-a-time"),
|
|
85
|
+
context_retention_ms=_field(
|
|
86
|
+
settings, "context_retention_ms", DEFAULT_CONTEXT_RETENTION_MS
|
|
87
|
+
),
|
|
88
|
+
document_retention_ms=_field(settings, "document_retention_ms", 600_000), )
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def add_tools(tx: Transaction, conversation_id: ConversationId, added: list[str]) -> None:
|
|
92
|
+
"""The ``addTools`` of a tool round, applied to ``tools`` and ``modelTools`` for the next request.
|
|
93
|
+
|
|
94
|
+
An array gets each name it lacks appended, and ``{ remove }`` loses the names. An unset ``tools``
|
|
95
|
+
already enables every tool, so nothing is written for it. An unset or object ``modelTools`` also
|
|
96
|
+
gets each name appended to its ``add``, which loads deferred tools and has no effect for others.
|
|
97
|
+
By name, without the registry: a tool that is not registered now is offered once it is again.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
state = await tx.doc(AgentDoc, conversation_id)
|
|
101
|
+
tools = state.get("tools")
|
|
102
|
+
if isinstance(tools, list) or hasattr(tools, "append"):
|
|
103
|
+
for name in added:
|
|
104
|
+
if name not in tools:
|
|
105
|
+
tools.append(name)
|
|
106
|
+
elif tools is not None:
|
|
107
|
+
removals = [item for item in tools.get("remove") or () if item not in added]
|
|
108
|
+
if len(removals) != len(tools.get("remove") or ()):
|
|
109
|
+
tools["remove"] = removals
|
|
110
|
+
model_tools = state.get("modelTools")
|
|
111
|
+
if isinstance(model_tools, list) or hasattr(model_tools, "append"):
|
|
112
|
+
for name in added:
|
|
113
|
+
if name not in model_tools:
|
|
114
|
+
model_tools.append(name)
|
|
115
|
+
return
|
|
116
|
+
current_removals = (model_tools.get("remove") if model_tools else None) or ()
|
|
117
|
+
removals = [item for item in current_removals if item not in added]
|
|
118
|
+
additions = list((model_tools.get("add") if model_tools else None) or ())
|
|
119
|
+
for name in added:
|
|
120
|
+
if name not in additions:
|
|
121
|
+
additions.append(name)
|
|
122
|
+
state["modelTools"] = {**({"remove": removals} if removals else {}), "add": additions}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def configure(tx: Transaction, conversation_id: ConversationId, change: Mapping[str, Any]) -> None:
|
|
126
|
+
"""Apply one change to ``pi.agent``: a given field replaces the stored one, ``None`` clears it."""
|
|
127
|
+
|
|
128
|
+
state = await tx.doc(AgentDoc, conversation_id)
|
|
129
|
+
for key, value in change.items():
|
|
130
|
+
field = _CHANGE_FIELDS.get(key, key)
|
|
131
|
+
if field not in _AGENT_FIELDS:
|
|
132
|
+
continue
|
|
133
|
+
if value is None:
|
|
134
|
+
state.pop(field, None)
|
|
135
|
+
elif field in ("extensions", "tools", "modelTools"):
|
|
136
|
+
state[field] = _names(value)
|
|
137
|
+
else:
|
|
138
|
+
state[field] = value
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_CHANGE_FIELDS = {
|
|
142
|
+
"thinking_level": "thinkingLevel",
|
|
143
|
+
"model_tools": "modelTools",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_AGENT_FIELDS = frozenset(
|
|
147
|
+
{"model", "thinkingLevel", "extensions", "tools", "modelTools", "instructions", "cwd"}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _names(value: Any) -> Any:
|
|
152
|
+
"""Store names, not objects, for an agent selection change; a dict keeps its list fields."""
|
|
153
|
+
|
|
154
|
+
if isinstance(value, Mapping):
|
|
155
|
+
return {key: [_name(item) for item in items] for key, items in value.items()}
|
|
156
|
+
if isinstance(value, (list, tuple)):
|
|
157
|
+
return [_name(item) for item in value]
|
|
158
|
+
return value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _name(item: Any) -> Any:
|
|
162
|
+
return getattr(item, "name", item)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def create_agent(tx: Transaction, conversation: ConversationRecord) -> None:
|
|
166
|
+
"""Built-in part of every commit that creates a conversation: copy the owner's agent state."""
|
|
167
|
+
|
|
168
|
+
if "parent" in conversation:
|
|
169
|
+
return
|
|
170
|
+
state = await tx.doc(AgentDoc, conversation["id"])
|
|
171
|
+
owner = conversation.get("owner")
|
|
172
|
+
if owner is None:
|
|
173
|
+
return
|
|
174
|
+
owner_state = await tx.doc(AgentDoc, owner["conversationId"])
|
|
175
|
+
state.update(cast(Mapping[str, Any], owner_state))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def agent_hooks(agent: Agent, task_name: str) -> list[Mapping[str, Any]]:
|
|
179
|
+
"""Handlers of the selected extensions' hooks for a task name, in extension order."""
|
|
180
|
+
|
|
181
|
+
return [
|
|
182
|
+
hook.handlers for extension in agent.extensions for hook in extension.hooks if hook.task == task_name
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def resolve_agent(
|
|
187
|
+
state: Mapping[str, Any] | None,
|
|
188
|
+
snapshot: RegistrySnapshot,
|
|
189
|
+
settings: Settings,
|
|
190
|
+
report: Callable[[BaseException], None],
|
|
191
|
+
) -> Agent:
|
|
192
|
+
"""Resolve an agent from its stored state, a registry snapshot, and resolved settings."""
|
|
193
|
+
|
|
194
|
+
stored = dict(state or {})
|
|
195
|
+
extensions = _select_extensions(stored.get("extensions"), snapshot, settings)
|
|
196
|
+
|
|
197
|
+
composed: dict[str, ToolRegistration] = {}
|
|
198
|
+
for extension in extensions:
|
|
199
|
+
for tool in extension.tools:
|
|
200
|
+
composed[tool.name] = tool
|
|
201
|
+
sections: dict[str, PromptSection] = {}
|
|
202
|
+
for extension in extensions:
|
|
203
|
+
for section in extension.sections:
|
|
204
|
+
sections[section.key] = section
|
|
205
|
+
for extension in extensions:
|
|
206
|
+
for wrap in extension.wraps:
|
|
207
|
+
if wrap.tool is not None and wrap.wrap is not None:
|
|
208
|
+
_apply_wrap(composed, wrap.tool, wrap.wrap, lambda tool: tool.name, report)
|
|
209
|
+
elif wrap.section is not None and wrap.wrap is not None:
|
|
210
|
+
_apply_wrap(sections, wrap.section, wrap.wrap, lambda section: section.key, report)
|
|
211
|
+
|
|
212
|
+
enabled = _filter_tools(list(composed.values()), stored.get("tools"))
|
|
213
|
+
model_callable = [tool for tool in enabled if tool.callers is None or "model" in tool.callers]
|
|
214
|
+
tools = _offered_tools(model_callable, stored.get("modelTools"))
|
|
215
|
+
offered = {tool.name for tool in tools}
|
|
216
|
+
deferred = [tool for tool in model_callable if tool.deferred and tool.name not in offered]
|
|
217
|
+
callable_tools = [tool for tool in enabled if tool.callers is None or "tools" in tool.callers]
|
|
218
|
+
|
|
219
|
+
instructions = stored.get("instructions")
|
|
220
|
+
agent_sections = list(sections.values())
|
|
221
|
+
if instructions is not None:
|
|
222
|
+
agent_sections.append(
|
|
223
|
+
PromptSection(key=INSTRUCTIONS_KEY, render=lambda *_args: instructions)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return Agent(
|
|
227
|
+
model=stored.get("model"),
|
|
228
|
+
thinking_level=stored.get("thinkingLevel", "off"),
|
|
229
|
+
extensions=extensions,
|
|
230
|
+
tools=tools,
|
|
231
|
+
callable=callable_tools,
|
|
232
|
+
deferred=deferred,
|
|
233
|
+
sections=agent_sections,
|
|
234
|
+
instructions=instructions,
|
|
235
|
+
cwd=stored.get("cwd"),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _apply_wrap(
|
|
240
|
+
items: dict[str, Any],
|
|
241
|
+
target: str,
|
|
242
|
+
wrap: Callable[..., Any],
|
|
243
|
+
name_of: Callable[[Any], str],
|
|
244
|
+
report: Callable[[BaseException], None],
|
|
245
|
+
) -> None:
|
|
246
|
+
item = items.get(target)
|
|
247
|
+
if item is None:
|
|
248
|
+
return
|
|
249
|
+
try:
|
|
250
|
+
wrapped = wrap(item)
|
|
251
|
+
renamed = name_of(wrapped)
|
|
252
|
+
if renamed != target:
|
|
253
|
+
raise ValueError(f"Wrapper renamed {target} to {renamed}")
|
|
254
|
+
items[target] = wrapped
|
|
255
|
+
except Exception as error:
|
|
256
|
+
items.pop(target, None)
|
|
257
|
+
report(error)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _offered_tools(
|
|
261
|
+
tools: list[ToolRegistration],
|
|
262
|
+
filter_value: Any,
|
|
263
|
+
) -> list[ToolRegistration]:
|
|
264
|
+
if isinstance(filter_value, (list, tuple)):
|
|
265
|
+
return _filter_tools(tools, filter_value)
|
|
266
|
+
removed = _name_set(_field(filter_value, "remove", []))
|
|
267
|
+
kept = [tool for tool in tools if not tool.deferred and tool.name not in removed]
|
|
268
|
+
by_name = {tool.name: tool for tool in tools}
|
|
269
|
+
loaded: list[ToolRegistration] = []
|
|
270
|
+
for name in _unique_names(_field(filter_value, "add", [])):
|
|
271
|
+
tool = by_name.get(name)
|
|
272
|
+
if tool is not None and tool.deferred and name not in removed:
|
|
273
|
+
loaded.append(tool)
|
|
274
|
+
return [*kept, *loaded]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _filter_tools(tools: list[ToolRegistration], filter_value: Any) -> list[ToolRegistration]:
|
|
278
|
+
if filter_value is None:
|
|
279
|
+
return tools
|
|
280
|
+
if isinstance(filter_value, (list, tuple)):
|
|
281
|
+
by_name = {tool.name: tool for tool in tools}
|
|
282
|
+
return [by_name[name] for name in _unique_names(filter_value) if name in by_name]
|
|
283
|
+
removed = _name_set(_field(filter_value, "remove", []))
|
|
284
|
+
return [tool for tool in tools if tool.name not in removed]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _unique_names(items: Any) -> list[str]:
|
|
288
|
+
"""Names in order, a repeated name only at its first position."""
|
|
289
|
+
|
|
290
|
+
names: list[str] = []
|
|
291
|
+
seen: set[str] = set()
|
|
292
|
+
for item in items:
|
|
293
|
+
name = _name(item)
|
|
294
|
+
if name not in seen:
|
|
295
|
+
seen.add(name)
|
|
296
|
+
names.append(name)
|
|
297
|
+
return names
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _name_set(items: Any) -> set[str]:
|
|
301
|
+
return {_name(item) for item in items}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _select_extensions(
|
|
305
|
+
stored: Any,
|
|
306
|
+
snapshot: RegistrySnapshot,
|
|
307
|
+
settings: Settings,
|
|
308
|
+
) -> list[Extension]:
|
|
309
|
+
if isinstance(stored, (list, tuple)):
|
|
310
|
+
selected = _unique_names(stored)
|
|
311
|
+
else:
|
|
312
|
+
if settings.extensions is not None:
|
|
313
|
+
base = _unique_names(settings.extensions)
|
|
314
|
+
else:
|
|
315
|
+
base = _unique_names(snapshot.installed())
|
|
316
|
+
edit = stored if isinstance(stored, Mapping) else {}
|
|
317
|
+
removed = _name_set(edit.get("remove", []))
|
|
318
|
+
selected = [
|
|
319
|
+
name
|
|
320
|
+
for name in _unique_names([*base, *edit.get("add", [])])
|
|
321
|
+
if name not in removed
|
|
322
|
+
]
|
|
323
|
+
extensions: list[Extension] = []
|
|
324
|
+
for name in selected:
|
|
325
|
+
extension = snapshot.extension(name)
|
|
326
|
+
if extension is not None:
|
|
327
|
+
extensions.append(extension)
|
|
328
|
+
return extensions
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"AgentDoc",
|
|
333
|
+
"DEFAULT_COMPACTION_POLICY",
|
|
334
|
+
"DEFAULT_CONTEXT_RETENTION_MS",
|
|
335
|
+
"DEFAULT_PROGRESS_POLICY",
|
|
336
|
+
"DEFAULT_RETRY_POLICY",
|
|
337
|
+
"INSTRUCTIONS_KEY",
|
|
338
|
+
"add_tools",
|
|
339
|
+
"agent_hooks",
|
|
340
|
+
"configure",
|
|
341
|
+
"create_agent",
|
|
342
|
+
"resolve_agent",
|
|
343
|
+
"resolve_settings",
|
|
344
|
+
]
|