polyforge-v3 3.0.16__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.
- polyforge_v3/__init__.py +4 -0
- polyforge_v3/aihub/__init__.py +5 -0
- polyforge_v3/aihub/_bearer.py +20 -0
- polyforge_v3/aihub/adapter.py +302 -0
- polyforge_v3/aihub/client.py +584 -0
- polyforge_v3/aihub/errors.py +70 -0
- polyforge_v3/aihub/http_const.py +49 -0
- polyforge_v3/aihub/models.py +477 -0
- polyforge_v3/aihub/session_file.py +220 -0
- polyforge_v3/auth.py +74 -0
- polyforge_v3/cli/__init__.py +49 -0
- polyforge_v3/cli/admin.py +109 -0
- polyforge_v3/cli/doctor.py +1280 -0
- polyforge_v3/cli/init.py +182 -0
- polyforge_v3/config/__init__.py +187 -0
- polyforge_v3/core/__init__.py +0 -0
- polyforge_v3/core/artifacts.py +95 -0
- polyforge_v3/core/conflicts.py +58 -0
- polyforge_v3/core/event_schemas/__init__.py +43 -0
- polyforge_v3/core/event_schemas/attempt_completed.schema.json +15 -0
- polyforge_v3/core/event_schemas/attempt_lease_renewed.schema.json +12 -0
- polyforge_v3/core/event_schemas/attempt_paused.schema.json +11 -0
- polyforge_v3/core/event_schemas/attempt_started.schema.json +26 -0
- polyforge_v3/core/event_schemas/attempt_taken_over.schema.json +14 -0
- polyforge_v3/core/event_schemas/auth_revoked.schema.json +17 -0
- polyforge_v3/core/event_schemas/branch_force_pushed.schema.json +15 -0
- polyforge_v3/core/event_schemas/cleanup_failed.schema.json +15 -0
- polyforge_v3/core/event_schemas/commit.schema.json +14 -0
- polyforge_v3/core/event_schemas/conflict_prediction_emitted.schema.json +35 -0
- polyforge_v3/core/event_schemas/conflict_prediction_overridden.schema.json +37 -0
- polyforge_v3/core/event_schemas/conflict_resolution_assisted.schema.json +20 -0
- polyforge_v3/core/event_schemas/error.schema.json +14 -0
- polyforge_v3/core/event_schemas/external_artifact_orphaned.schema.json +17 -0
- polyforge_v3/core/event_schemas/external_artifact_reconciled.schema.json +22 -0
- polyforge_v3/core/event_schemas/lock_acquired.schema.json +16 -0
- polyforge_v3/core/event_schemas/lock_released.schema.json +16 -0
- polyforge_v3/core/event_schemas/memory_redacted.schema.json +13 -0
- polyforge_v3/core/event_schemas/note.schema.json +11 -0
- polyforge_v3/core/event_schemas/pr_opened.schema.json +16 -0
- polyforge_v3/core/event_schemas/pr_updated.schema.json +17 -0
- polyforge_v3/core/event_schemas/prepared_workspace.schema.json +15 -0
- polyforge_v3/core/event_schemas/push.schema.json +14 -0
- polyforge_v3/core/event_schemas/push_base_check_skipped.schema.json +13 -0
- polyforge_v3/core/event_schemas/push_blocked_base_moved.schema.json +15 -0
- polyforge_v3/core/event_schemas/pushed_tag.schema.json +13 -0
- polyforge_v3/core/event_schemas/release_included_wi.schema.json +11 -0
- polyforge_v3/core/event_schemas/repo_blocked.schema.json +16 -0
- polyforge_v3/core/event_schemas/repo_committed.schema.json +14 -0
- polyforge_v3/core/event_schemas/repo_prepared.schema.json +15 -0
- polyforge_v3/core/event_schemas/repo_recovered.schema.json +24 -0
- polyforge_v3/core/event_schemas/skill_returned.schema.json +15 -0
- polyforge_v3/core/event_schemas/tagged_repo.schema.json +14 -0
- polyforge_v3/core/event_schemas/work_item_completed.schema.json +16 -0
- polyforge_v3/core/event_schemas/work_item_filed.schema.json +20 -0
- polyforge_v3/core/event_schemas/work_item_updated.schema.json +17 -0
- polyforge_v3/core/events.py +118 -0
- polyforge_v3/core/events_emit.py +65 -0
- polyforge_v3/core/lease.py +297 -0
- polyforge_v3/core/locks.py +44 -0
- polyforge_v3/core/lost_lease.py +372 -0
- polyforge_v3/core/memory.py +64 -0
- polyforge_v3/core/run_attempts.py +108 -0
- polyforge_v3/core/work_items.py +86 -0
- polyforge_v3/ids.py +21 -0
- polyforge_v3/logging.py +27 -0
- polyforge_v3/methodology/__init__.py +5 -0
- polyforge_v3/methodology/artifact_types.py +64 -0
- polyforge_v3/methodology/memory_adapter.py +85 -0
- polyforge_v3/methodology/schemas/plan.schema.json +31 -0
- polyforge_v3/methodology/schemas/retro.schema.json +30 -0
- polyforge_v3/methodology/schemas/review.schema.json +30 -0
- polyforge_v3/methodology/schemas/spec.schema.json +31 -0
- polyforge_v3/scenarios/__init__.py +0 -0
- polyforge_v3/scenarios/capabilities.py +40 -0
- polyforge_v3/scenarios/protocol.py +201 -0
- polyforge_v3/scenarios/registry.py +61 -0
- polyforge_v3/server/__init__.py +8 -0
- polyforge_v3/server/__main__.py +382 -0
- polyforge_v3/server/actor_cache.py +108 -0
- polyforge_v3/tools/__init__.py +32 -0
- polyforge_v3/tools/conflicts.py +107 -0
- polyforge_v3/tools/events.py +104 -0
- polyforge_v3/tools/lifecycle.py +262 -0
- polyforge_v3/tools/locks.py +45 -0
- polyforge_v3/tools/maintenance.py +210 -0
- polyforge_v3/tools/memory.py +92 -0
- polyforge_v3/tools/registry.py +105 -0
- polyforge_v3/tools/release.py +364 -0
- polyforge_v3/tools/scenarios.py +84 -0
- polyforge_v3-3.0.16.dist-info/METADATA +18 -0
- polyforge_v3-3.0.16.dist-info/RECORD +94 -0
- polyforge_v3-3.0.16.dist-info/WHEEL +5 -0
- polyforge_v3-3.0.16.dist-info/entry_points.txt +2 -0
- polyforge_v3-3.0.16.dist-info/top_level.txt +1 -0
polyforge_v3/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Per-request bearer token override via ContextVar.
|
|
2
|
+
|
|
3
|
+
Placed in `polyforge_v3.aihub` (not `server`) so `client.py` can import it
|
|
4
|
+
without creating a circular dependency. Both `server/__main__.py` (writer)
|
|
5
|
+
and `aihub/client.py` (reader) import from here.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
|
|
11
|
+
# Set to a non-None bearer string before an aihub client call to override the
|
|
12
|
+
# default Authorization header for that request. Reset to None (via
|
|
13
|
+
# ContextVar.reset) after the call returns. Thread / task safe because
|
|
14
|
+
# ContextVar is per-asyncio-Task.
|
|
15
|
+
_CURRENT_BEARER: ContextVar[str | None] = ContextVar("_current_bearer", default=None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_current_bearer() -> str | None:
|
|
19
|
+
"""Return the active per-request bearer override, or None."""
|
|
20
|
+
return _CURRENT_BEARER.get()
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""ProtocolAdapter — bridges AihubClient (Pydantic models) to the dict-based Protocol
|
|
2
|
+
that 1C composite tools expect.
|
|
3
|
+
|
|
4
|
+
design.md §10.1: 1C tools type the client by Protocol (kwargs-in / dict-out). 1B's
|
|
5
|
+
concrete AihubClient takes Pydantic request objects and returns Pydantic response objects.
|
|
6
|
+
This adapter wraps the concrete client, handles the Pydantic⟷dict translation, and stores
|
|
7
|
+
an optional AttemptCredential for mutating endpoints.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
from polyforge_v3.aihub.adapter import AihubClientProtocolAdapter
|
|
12
|
+
from polyforge_v3.aihub.client import AihubClient
|
|
13
|
+
|
|
14
|
+
concrete = AihubClient(cfg, session)
|
|
15
|
+
adapter = AihubClientProtocolAdapter(concrete)
|
|
16
|
+
adapter.set_cred(AttemptCredential(attempt_id=..., claim_epoch=..., session_secret=...))
|
|
17
|
+
|
|
18
|
+
# Now use adapter wherever 1C expects the AihubClient Protocol:
|
|
19
|
+
result = await adapter.create_work_item(project="marketplace", goal="fix x", ...)
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import warnings
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
from .client import AihubClient
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
# Runtime-lazy: at module load time `polyforge_v3.auth` is mid-loading
|
|
30
|
+
# (auth.py:18 triggers aihub/__init__ → this module → here). Top-level
|
|
31
|
+
# `from ..auth import AttemptCredential` would race auth's own class
|
|
32
|
+
# definition. PEP 563 annotations only need the symbol for typing tools.
|
|
33
|
+
from ..auth import AttemptCredential # noqa: F401
|
|
34
|
+
from .models import (
|
|
35
|
+
ClaimRequest,
|
|
36
|
+
CompleteAttemptRequest,
|
|
37
|
+
CompleteWorkItemRequest,
|
|
38
|
+
CreateWorkItemRequest,
|
|
39
|
+
DeclaredResource,
|
|
40
|
+
EmitEventRequest,
|
|
41
|
+
PauseAttemptRequest,
|
|
42
|
+
PredictConflictsRequest,
|
|
43
|
+
RenewLeaseRequest,
|
|
44
|
+
UpdateWorkItemRequest,
|
|
45
|
+
_LockSpec,
|
|
46
|
+
_SessionInfoFrag,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AihubClientProtocolAdapter:
|
|
51
|
+
"""Wrap the concrete AihubClient to satisfy 1C's kwargs-in / dict-out Protocol.
|
|
52
|
+
|
|
53
|
+
Maintains a stored AttemptCredential (set via set_cred() after claim) for
|
|
54
|
+
mutating endpoints that require it. Some 1C calls supply credentials as kwargs
|
|
55
|
+
directly; kwargs always override the stored credential.
|
|
56
|
+
|
|
57
|
+
Lifted from the wire e2e test (test_3stream_wire_e2e.py) into production so
|
|
58
|
+
the real server.py can use it when wiring 1B to 1C.
|
|
59
|
+
|
|
60
|
+
Finding 3 fix: the test had a local ProtocolAdapter; this is its canonical home.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, concrete: AihubClient) -> None:
|
|
64
|
+
self._c = concrete
|
|
65
|
+
self._cred: AttemptCredential | None = None
|
|
66
|
+
|
|
67
|
+
def set_cred(self, cred: AttemptCredential) -> None:
|
|
68
|
+
"""Store AttemptCredential after claim so mutating endpoints can use it."""
|
|
69
|
+
self._cred = cred
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# Work items
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
async def create_work_item(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
project: str | None,
|
|
79
|
+
goal: str,
|
|
80
|
+
scenario: str,
|
|
81
|
+
declared_resources: list[dict[str, Any]],
|
|
82
|
+
labels: list[str] | None = None,
|
|
83
|
+
priority: str = "normal",
|
|
84
|
+
external_share_type: str | None = None,
|
|
85
|
+
external_share_key: str | None = None,
|
|
86
|
+
parent_work_item_id: str | None = None,
|
|
87
|
+
metadata: dict[str, Any] | None = None,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
req = CreateWorkItemRequest(
|
|
90
|
+
project=project or "",
|
|
91
|
+
scenario=scenario,
|
|
92
|
+
goal=goal,
|
|
93
|
+
declared_resources=[
|
|
94
|
+
DeclaredResource.model_validate(r) for r in (declared_resources or [])
|
|
95
|
+
],
|
|
96
|
+
labels=labels,
|
|
97
|
+
priority=priority,
|
|
98
|
+
external_share_type=external_share_type,
|
|
99
|
+
external_share_key=external_share_key,
|
|
100
|
+
parent_work_item_id=parent_work_item_id,
|
|
101
|
+
metadata=metadata,
|
|
102
|
+
)
|
|
103
|
+
wi = await self._c.create_work_item(req)
|
|
104
|
+
return {"work_item_id": wi.id, **wi.model_dump()}
|
|
105
|
+
|
|
106
|
+
async def predict_conflicts(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
work_item_id: str | None,
|
|
110
|
+
declared_resources: list[dict[str, Any]],
|
|
111
|
+
project: str | None = None,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
if project is not None:
|
|
114
|
+
# Prefer the explicit project kwarg (canonical path)
|
|
115
|
+
effective_project = project
|
|
116
|
+
else:
|
|
117
|
+
# Fall back to URI parsing (deprecated — callers should pass project= explicitly)
|
|
118
|
+
warnings.warn(
|
|
119
|
+
"predict_conflicts: project derivation from URI deprecated; pass project= explicitly",
|
|
120
|
+
DeprecationWarning,
|
|
121
|
+
stacklevel=2,
|
|
122
|
+
)
|
|
123
|
+
effective_project = "marketplace"
|
|
124
|
+
for r in declared_resources:
|
|
125
|
+
uri = r.get("uri", "")
|
|
126
|
+
if uri.startswith("repo:"):
|
|
127
|
+
effective_project = uri.removeprefix("repo:")
|
|
128
|
+
break
|
|
129
|
+
req = PredictConflictsRequest(
|
|
130
|
+
project=effective_project,
|
|
131
|
+
work_item_id=work_item_id,
|
|
132
|
+
declared_resources=[
|
|
133
|
+
DeclaredResource.model_validate(r) for r in (declared_resources or [])
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
resp = await self._c.predict_conflicts(req)
|
|
137
|
+
return {
|
|
138
|
+
"severity": resp.severity,
|
|
139
|
+
"predictions": [p.model_dump() for p in resp.predictions],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async def claim_work_item(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
work_item_id: str,
|
|
146
|
+
idempotency_key: str,
|
|
147
|
+
session_info: dict[str, Any],
|
|
148
|
+
requested_locks: list[dict[str, Any]],
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
# session_secret: prefer session_info kwarg, fall back to client session
|
|
151
|
+
secret = (
|
|
152
|
+
session_info.get("session_secret")
|
|
153
|
+
or self._c.session.session_secret
|
|
154
|
+
)
|
|
155
|
+
machine_id = session_info.get("machine_id") or self._c.session.machine_id
|
|
156
|
+
req = ClaimRequest(
|
|
157
|
+
idempotency_key=idempotency_key,
|
|
158
|
+
session_info=_SessionInfoFrag(
|
|
159
|
+
machine_id=machine_id,
|
|
160
|
+
session_secret=secret,
|
|
161
|
+
),
|
|
162
|
+
requested_locks=[_LockSpec.model_validate(l) for l in requested_locks],
|
|
163
|
+
)
|
|
164
|
+
resp = await self._c.claim_work_item(work_item_id, req)
|
|
165
|
+
return {
|
|
166
|
+
"attempt_id": resp.attempt_id,
|
|
167
|
+
"claim_epoch": resp.claim_epoch,
|
|
168
|
+
"lease_until": resp.lease_until
|
|
169
|
+
if isinstance(resp.lease_until, str)
|
|
170
|
+
else str(resp.lease_until),
|
|
171
|
+
"session_secret": secret,
|
|
172
|
+
"machine_id": machine_id,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async def get_work_item(self, *, work_item_id: str) -> dict[str, Any]:
|
|
176
|
+
resp = await self._c.get_work_item(work_item_id)
|
|
177
|
+
return resp.model_dump(mode="json")
|
|
178
|
+
|
|
179
|
+
async def update_work_item(
|
|
180
|
+
self, *, work_item_id: str, patch_payload: dict[str, Any]
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
if self._cred is None:
|
|
183
|
+
raise RuntimeError(
|
|
184
|
+
"update_work_item requires AttemptCredential; call set_cred() first"
|
|
185
|
+
)
|
|
186
|
+
req = UpdateWorkItemRequest(
|
|
187
|
+
attempt_id=self._cred.attempt_id,
|
|
188
|
+
claim_epoch=self._cred.claim_epoch,
|
|
189
|
+
session_secret=self._cred.session_secret,
|
|
190
|
+
patch_payload=patch_payload,
|
|
191
|
+
)
|
|
192
|
+
wi = await self._c.update_work_item(work_item_id, req)
|
|
193
|
+
return {"work_item": wi.model_dump(mode="json")}
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Events
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
async def emit_event(
|
|
200
|
+
self,
|
|
201
|
+
*,
|
|
202
|
+
work_item_id: str,
|
|
203
|
+
event_type: str,
|
|
204
|
+
payload: dict[str, Any],
|
|
205
|
+
attempt_id: str | None = None,
|
|
206
|
+
claim_epoch: int | None = None,
|
|
207
|
+
session_secret: str | None = None,
|
|
208
|
+
pinned: bool = False,
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Emit an attempt-scoped event (requires AttemptCredential in body).
|
|
211
|
+
|
|
212
|
+
attempt_id, claim_epoch, session_secret may be passed as kwargs (1C pattern)
|
|
213
|
+
or resolved from the stored credential. If attempt_id is None the call raises
|
|
214
|
+
ValueError — ensure claim_work_item is called first to obtain AttemptCredential.
|
|
215
|
+
"""
|
|
216
|
+
eff_attempt_id = attempt_id
|
|
217
|
+
eff_claim_epoch = claim_epoch
|
|
218
|
+
eff_session_secret = session_secret
|
|
219
|
+
# Fall back to stored cred if kwargs not provided
|
|
220
|
+
if (eff_attempt_id is None or eff_claim_epoch is None or eff_session_secret is None) and self._cred is not None:
|
|
221
|
+
eff_attempt_id = eff_attempt_id or self._cred.attempt_id
|
|
222
|
+
eff_claim_epoch = eff_claim_epoch if eff_claim_epoch is not None else self._cred.claim_epoch
|
|
223
|
+
eff_session_secret = eff_session_secret or self._cred.session_secret
|
|
224
|
+
if eff_attempt_id is None:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"emit_event requires attempt_id; no credential set. "
|
|
227
|
+
"Call claim_work_item first to obtain AttemptCredential."
|
|
228
|
+
)
|
|
229
|
+
req = EmitEventRequest(
|
|
230
|
+
attempt_id=eff_attempt_id,
|
|
231
|
+
claim_epoch=eff_claim_epoch,
|
|
232
|
+
session_secret=eff_session_secret,
|
|
233
|
+
work_item_id=work_item_id,
|
|
234
|
+
event_type=event_type,
|
|
235
|
+
payload=payload,
|
|
236
|
+
pinned=pinned,
|
|
237
|
+
)
|
|
238
|
+
resp = await self._c.emit_event(req)
|
|
239
|
+
return {"event_id": resp.event_id}
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Attempts
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
async def complete_attempt(
|
|
246
|
+
self,
|
|
247
|
+
*,
|
|
248
|
+
attempt_id: str,
|
|
249
|
+
claim_epoch: int,
|
|
250
|
+
session_secret: str,
|
|
251
|
+
status: str,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
req = CompleteAttemptRequest(
|
|
254
|
+
attempt_id=attempt_id,
|
|
255
|
+
claim_epoch=claim_epoch,
|
|
256
|
+
session_secret=session_secret,
|
|
257
|
+
status=status,
|
|
258
|
+
)
|
|
259
|
+
resp = await self._c.complete_attempt(attempt_id, req)
|
|
260
|
+
return {"ok": resp.ok}
|
|
261
|
+
|
|
262
|
+
async def pause_attempt(
|
|
263
|
+
self, *, attempt_id: str, claim_epoch: int,
|
|
264
|
+
session_secret: str, reason: str,
|
|
265
|
+
) -> dict[str, Any]:
|
|
266
|
+
req = PauseAttemptRequest(
|
|
267
|
+
attempt_id=attempt_id,
|
|
268
|
+
claim_epoch=claim_epoch,
|
|
269
|
+
session_secret=session_secret,
|
|
270
|
+
reason=reason,
|
|
271
|
+
)
|
|
272
|
+
resp = await self._c.pause_attempt(attempt_id, req)
|
|
273
|
+
return {"ok": resp.ok}
|
|
274
|
+
|
|
275
|
+
async def renew_lease(
|
|
276
|
+
self, *, attempt_id: str, claim_epoch: int, session_secret: str
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
resp = await self._c.renew_lease(
|
|
279
|
+
attempt_id, RenewLeaseRequest(claim_epoch=claim_epoch, session_secret=session_secret)
|
|
280
|
+
)
|
|
281
|
+
lu = resp.lease_until
|
|
282
|
+
return {"lease_until": lu if isinstance(lu, str) else str(lu)}
|
|
283
|
+
|
|
284
|
+
async def whoami(self) -> dict[str, Any]:
|
|
285
|
+
"""Caller identity for admin-gated scenarios (release etc.)."""
|
|
286
|
+
resp = await self._c.whoami()
|
|
287
|
+
return resp.model_dump()
|
|
288
|
+
|
|
289
|
+
async def reconcile_artifacts(self, *, work_item_id: str) -> list[dict[str, Any]]:
|
|
290
|
+
"""Fail-loud: composite tools must call reconcile directly, not via this adapter.
|
|
291
|
+
|
|
292
|
+
design §10.1 (B6): composite tools should call
|
|
293
|
+
polyforge_v3_coding.artifacts.reconcile directly (async). This adapter has
|
|
294
|
+
no implementation by design — calling it indicates a caller bug.
|
|
295
|
+
"""
|
|
296
|
+
raise NotImplementedError(
|
|
297
|
+
"composite tools should call polyforge_v3_coding.artifacts.reconcile "
|
|
298
|
+
"directly (async); adapter has no impl by design"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
__all__ = ["AihubClientProtocolAdapter"]
|