mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
## Thinking Path > - Paperclip manages agents and their work. > - Native Runner providers need verified executables before they can run. > - Google Antigravity ships a native ACP server and a helper executable. > - An archive hash alone does not verify the installed runtime or its child process cleanup. > - This pull request adds a pinned installer and a small, verified compatibility patch. > - Later pull requests add Runner execution, account setup, and the recorded user journey eval. ## Linked Issues or Issue Description Refs: #12503, #14938. Those pull requests use the legacy CLI adapter. This stack adds native Runner ACP support. **Agent or provider** Google Antigravity ACP 1.4.3. **Why this adapter is useful** Users need a reproducible runtime before they can select Antigravity for a Paperclip agent. **How the agent is invoked** The installer prepares the native ACP server and helper. This pull request does not enable task execution. Default execution remains closed pending qualification. ## What Changed - Pin the complete macOS ARM64 runtime closure. Keep other platforms as inventory only. - Verify archive entries, installed bytes, ownership, and cache layout. - Patch exact upstream code anchors for model admission, instruction loading, native process ownership, and per-call file tool completion. - Package a self-contained installer and expose `paperclipai runtime setup antigravity`. - Record candidate limitations and historical acceptance evidence. ## Verification - Runtime provisioning, packaging, metadata, and patch support tests: 62 passed. The separate exact pinned native patch run passed all 5 tests with no skips and no Google authentication. - CLI runtime setup tests: 5 passed. - Map contract check passed. Staged diff whitespace check passed. - The first test command ran before dependency installation and failed on missing esbuild. The same command passed after installation. - Current master has a stale lockfile. Dependencies were resolved locally with scripts disabled. The generated lockfile is excluded from this pull request. - Final head: required `ci / verify` and `ci / e2e` passed, and Greptile is 5/5 with both review threads resolved. The optional external Paperclip Review remains queued. Two original timing failures are retained; one failed-job rerun passed without source or deadline changes. ## Risks - The patch depends on exact upstream native code. A new upstream release must get a new verified closure. - Only macOS ARM64 has a complete verified runtime. Installation does not qualify authentication, task execution, or other platforms. - The installer downloads official Google assets when the verified cache is absent. ## Model Used OpenAI Codex (GPT-6). The host did not expose the exact model revision or context limit. The model used tools for code edits, test execution, and review. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
443 lines
20 KiB
Python
443 lines
20 KiB
Python
"""Paperclip-owned admission for the pinned Antigravity 1.4.3 native runtime.
|
|
|
|
Loaded inside Google's hermetic distribution. No credentials or prompt content
|
|
are persisted in receipts. Native creation must acknowledge the exact config
|
|
before the ACP client receives authority to send a managed prompt.
|
|
"""
|
|
import asyncio
|
|
import contextvars
|
|
import ctypes
|
|
import dataclasses
|
|
import functools
|
|
import hashlib
|
|
import os
|
|
import stat
|
|
import re
|
|
import uuid
|
|
|
|
from acp import exceptions
|
|
from google3.cloud.developer_experience.antigravity_extensions.acp_server import request_meta
|
|
from google3.cloud.developer_experience.antigravity_extensions.acp_server import system_prompt
|
|
from google3.cloud.developer_experience.antigravity_extensions.acp_server import hooks as acp_hooks
|
|
from google3.cloud.developer_experience.antigravity_extensions.acp_server import tool_filter
|
|
from google3.cloud.developer_experience.antigravity_extensions.acp_server.oauth.credential_manager import OAuthCredentialManager
|
|
|
|
SCHEMA = "paperclip.antigravity.admission.v1"
|
|
MODELS_SCHEMA = "paperclip.antigravity.models.v1"
|
|
_CONTEXT = contextvars.ContextVar("paperclip_antigravity_admission", default=None)
|
|
|
|
|
|
def _fail(message):
|
|
raise exceptions.RequestError(code=-32602, message="Paperclip admission: " + message)
|
|
|
|
|
|
def _read_request(kwargs):
|
|
meta = request_meta.antigravity_meta_from_request_kwargs(kwargs)
|
|
value = meta.get("paperclipAdmission")
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, dict) or set(value) != {
|
|
"schema", "nonce", "model", "digest", "byteLength"
|
|
}:
|
|
_fail("invalid request fields")
|
|
if value["schema"] != SCHEMA:
|
|
_fail("unsupported schema")
|
|
for key in ("nonce", "model"):
|
|
if not isinstance(value[key], str) or not 1 <= len(value[key]) <= 512:
|
|
_fail("invalid " + key)
|
|
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(value["digest"])):
|
|
_fail("invalid instruction digest")
|
|
prompt = meta.get("systemPrompt")
|
|
if not isinstance(prompt, dict) or set(prompt) != {"append"} or not isinstance(prompt["append"], str):
|
|
_fail("exact systemPrompt.append is required, including empty content")
|
|
content = prompt["append"].encode("utf-8")
|
|
if type(value["byteLength"]) is not int or value["byteLength"] != len(content):
|
|
_fail("instruction byte length mismatch")
|
|
if value["digest"] != "sha256:" + hashlib.sha256(content).hexdigest():
|
|
_fail("instruction digest mismatch")
|
|
return {**value, "content": prompt["append"]}
|
|
|
|
|
|
def prepare_native_config(config, strategy):
|
|
"""Inspect the protobuf that will actually be sent to this owned harness."""
|
|
admission = _CONTEXT.get()
|
|
if admission is None:
|
|
return None
|
|
executable_path = ctypes.create_string_buffer(4096)
|
|
path_size = ctypes.c_uint32(len(executable_path))
|
|
get_executable_path = ctypes.CDLL("/usr/lib/libSystem.B.dylib")._NSGetExecutablePath
|
|
get_executable_path.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
|
|
get_executable_path.restype = ctypes.c_int
|
|
if get_executable_path(executable_path, ctypes.byref(path_size)) != 0:
|
|
_fail("cannot resolve the owned native executable path")
|
|
helper = os.path.join(os.path.dirname(os.path.realpath(os.fsdecode(executable_path.value))), "localharness_external")
|
|
helper_stat = os.lstat(helper)
|
|
if not stat.S_ISREG(helper_stat.st_mode) or helper_stat.st_nlink != 1:
|
|
_fail("native helper must be the immutable sibling executable")
|
|
with open(helper, "rb") as helper_file:
|
|
helper_digest = hashlib.file_digest(helper_file, "sha256").hexdigest()
|
|
if helper_digest != "9e8fdc6e20a3262ca0a771850ca4a8414ca839a642cc964ec5c547cc2c33c200":
|
|
_fail("native sibling helper digest differs")
|
|
# The SDK resolves this attribute in its constructor. Updating the global
|
|
# environment alone cannot replace that already-cached executable path.
|
|
strategy._binary_path = helper
|
|
if strategy._env is not None:
|
|
strategy._env = {**strategy._env, "ANTIGRAVITY_HARNESS_PATH": helper}
|
|
os.environ["ANTIGRAVITY_HARNESS_PATH"] = helper
|
|
targets = [model for model in config.models if 1 in model.types]
|
|
if len(targets) != 1 or targets[0].name != admission["model"]:
|
|
_fail("native text model differs from the exact requested model")
|
|
instructions = config.system_instructions
|
|
expected = admission["content"]
|
|
if expected:
|
|
if instructions.WhichOneof("type") != "appended":
|
|
_fail("native instruction layering differs")
|
|
sections = instructions.appended.appended_sections
|
|
if len(sections) != 1 or sections[0].content != expected:
|
|
_fail("native instruction content differs")
|
|
elif config.HasField("system_instructions"):
|
|
# Empty content means no appended section, with the normal native defaults.
|
|
if instructions.WhichOneof("type") != "appended" or any(section.content for section in instructions.appended.appended_sections):
|
|
_fail("native empty instruction content differs")
|
|
if not config.cascade_id:
|
|
_fail("native session identity is missing")
|
|
if config.harness_side_tools.subagents.enabled or config.harness_side_tools.run_workflow.enabled or config.custom_subagents:
|
|
_fail("managed subagents, workflows, and browser agents are unavailable")
|
|
if config.harness_side_tools.schedule.enabled:
|
|
_fail("managed timers and recurring schedules belong to Paperclip")
|
|
return {
|
|
**admission,
|
|
"sessionId": config.cascade_id,
|
|
"connectionId": uuid.uuid4().hex,
|
|
"configBytes": config.SerializeToString(deterministic=True),
|
|
}
|
|
|
|
|
|
def acknowledge_native_config(prepared, event, config):
|
|
"""A receipt exists only after the same native connection acknowledges init."""
|
|
if prepared is None:
|
|
return None
|
|
if not event.HasField("initialize_conversation_response"):
|
|
_fail("native initialization acknowledgment is missing")
|
|
if event.initialize_conversation_response.cascade_id != prepared["sessionId"]:
|
|
_fail("native initialization session differs")
|
|
if config.SerializeToString(deterministic=True) != prepared["configBytes"]:
|
|
_fail("native initialization config changed")
|
|
return {key: prepared[key] for key in (
|
|
"schema", "nonce", "sessionId", "connectionId", "model", "digest", "byteLength"
|
|
)}
|
|
|
|
|
|
def _receipt(adapter, session_id, admission):
|
|
session = adapter._sessions.get(session_id)
|
|
if session is None:
|
|
_fail("native session is missing")
|
|
strategy = getattr(session.agent, "_strategy", None)
|
|
receipt = getattr(strategy, "_paperclip_native_admission", None)
|
|
if not isinstance(receipt, dict) or receipt.get("sessionId") != session_id:
|
|
_fail("native content acknowledgment is missing")
|
|
for key in ("schema", "nonce", "model", "digest", "byteLength"):
|
|
if receipt.get(key) != admission[key]:
|
|
_fail("native content acknowledgment differs: " + key)
|
|
if session.mode_id != "default" or session.system_prompt != admission["content"]:
|
|
_fail("managed mode or instruction state changed")
|
|
if session.current_model_id != admission["model"] or session.agent_model_id != admission["model"]:
|
|
_fail("selected model differs from initialized model")
|
|
return dict(receipt)
|
|
|
|
|
|
def _add_receipt(result, receipt):
|
|
result.field_meta = {**(result.field_meta or {}), "paperclipAntigravityAdmission": receipt}
|
|
return result
|
|
|
|
|
|
def install(adapter_type):
|
|
if adapter_type.__dict__.get("_paperclip_admission_installed", False):
|
|
return
|
|
original_initialize = adapter_type.initialize
|
|
original_extract = system_prompt.extract_system_prompt
|
|
original_list_models = adapter_type._list_available_models
|
|
original_create_config = adapter_type._create_agent_config
|
|
original_rebuild = adapter_type._rebuild_session_agent
|
|
original_replace = adapter_type._maybe_replace_unavailable_model
|
|
original_prompt = adapter_type.prompt
|
|
original_cleanup = adapter_type.cleanup
|
|
original_ext = getattr(adapter_type, "ext_method", None)
|
|
original_oauth_mgr = getattr(adapter_type, "_ensure_oauth_mgr", None)
|
|
original_hooks_config = acp_hooks.load_hooks_config
|
|
original_hooks_config_async = acp_hooks.load_hooks_config_async
|
|
original_has_workspace_hooks = acp_hooks.has_workspace_hooks
|
|
|
|
# Bind this to the manager, because later token use occurs in background
|
|
# threads without the request ContextVar. Explicit standalone authenticate
|
|
# uses a separate, unflagged manager and retains its interactive login.
|
|
if not getattr(OAuthCredentialManager._interactive_login_and_store, "_paperclip_managed_guard", False):
|
|
original_interactive_login = OAuthCredentialManager._interactive_login_and_store
|
|
|
|
@functools.wraps(original_interactive_login)
|
|
def interactive_login(manager):
|
|
if getattr(manager, "_paperclip_noninteractive", False):
|
|
raise ValueError("Google sign-in must be renewed through Paperclip before this managed task can continue")
|
|
return original_interactive_login(manager)
|
|
|
|
interactive_login._paperclip_managed_guard = True
|
|
OAuthCredentialManager._interactive_login_and_store = interactive_login
|
|
|
|
def ensure_oauth_mgr(self):
|
|
manager = original_oauth_mgr(self)
|
|
if _CONTEXT.get() is not None or getattr(self, "_paperclip_managed_sessions", {}):
|
|
manager._paperclip_noninteractive = True
|
|
return manager
|
|
|
|
def hooks_config(*args, **kwargs):
|
|
return {} if _CONTEXT.get() is not None else original_hooks_config(*args, **kwargs)
|
|
|
|
async def hooks_config_async(*args, **kwargs):
|
|
return {} if _CONTEXT.get() is not None else await original_hooks_config_async(*args, **kwargs)
|
|
|
|
def has_workspace_hooks(*args, **kwargs):
|
|
return False if _CONTEXT.get() is not None else original_has_workspace_hooks(*args, **kwargs)
|
|
|
|
@functools.wraps(original_initialize)
|
|
async def initialize(self, *args, **kwargs):
|
|
if getattr(self, "_paperclip_managed_sessions", {}):
|
|
_fail("cannot renegotiate an initialized managed connection")
|
|
result = await original_initialize(self, *args, **kwargs)
|
|
meta = result.agent_capabilities.field_meta or {}
|
|
meta.setdefault("antigravity", {})["paperclipAdmission"] = 1
|
|
meta["antigravity"]["paperclipModels"] = 1
|
|
result.agent_capabilities.field_meta = meta
|
|
self._paperclip_managed_sessions = {}
|
|
return result
|
|
|
|
def extract(kwargs):
|
|
admission = _CONTEXT.get()
|
|
return admission["content"] if admission is not None else original_extract(kwargs)
|
|
|
|
def list_models(self, current_model_id):
|
|
admission = _CONTEXT.get()
|
|
selected = admission["model"] if admission is not None else current_model_id
|
|
result = original_list_models(self, selected)
|
|
if admission is not None and result.current_model_id != selected:
|
|
_fail("provider rejected the exact requested model")
|
|
return result
|
|
|
|
@functools.wraps(original_create_config)
|
|
async def create_config(self, *args, **kwargs):
|
|
admission = _CONTEXT.get()
|
|
if admission is not None and (
|
|
kwargs.get("model_id") != admission["model"]
|
|
or kwargs.get("system_instructions") != admission["content"]
|
|
):
|
|
_fail("native model or instruction input differs")
|
|
if admission is not None:
|
|
requested_filter = kwargs.get("tool_filter", tool_filter.ToolFilterConfig())
|
|
if not isinstance(requested_filter, tool_filter.ToolFilterConfig):
|
|
_fail("invalid managed native tool filter")
|
|
kwargs["tool_filter"] = dataclasses.replace(requested_filter, disabled_tools=tuple(dict.fromkeys((*(requested_filter.disabled_tools or ()), "invoke_subagent", "run_workflow", "schedule"))))
|
|
result = await original_create_config(self, *args, **kwargs)
|
|
if admission is not None:
|
|
config = result[0] if isinstance(result, tuple) else result
|
|
external_hooks = (acp_hooks.ExternalPreInvocationHook, acp_hooks.ExternalPostInvocationHook, acp_hooks.ExternalPreToolCallDecideHook, acp_hooks.ExternalPostToolCallHook, acp_hooks.ExternalOnSessionEndHook)
|
|
if any(isinstance(hook, external_hooks) for hook in config.hooks):
|
|
_fail("managed workspace command hooks are unavailable")
|
|
return result
|
|
|
|
def wrap_open(original, is_new):
|
|
@functools.wraps(original)
|
|
async def open_session(self, *args, **kwargs):
|
|
admission = _read_request(kwargs)
|
|
session_id = None if is_new else kwargs.get("session_id", args[0] if args else None)
|
|
managed = getattr(self, "_paperclip_managed_sessions", {})
|
|
if admission is None:
|
|
if session_id in managed:
|
|
_fail("managed reload requires a fresh exact content acknowledgment")
|
|
return await original(self, *args, **kwargs)
|
|
if any(item["nonce"] == admission["nonce"] for item in managed.values()):
|
|
_fail("request nonce was already used")
|
|
token = _CONTEXT.set(admission)
|
|
try:
|
|
result = await original(self, *args, **kwargs)
|
|
session_id = result.session_id if is_new else session_id
|
|
receipt = _receipt(self, session_id, admission)
|
|
managed[session_id] = admission
|
|
self._paperclip_managed_sessions = managed
|
|
return _add_receipt(result, receipt)
|
|
finally:
|
|
_CONTEXT.reset(token)
|
|
return open_session
|
|
|
|
@functools.wraps(original_rebuild)
|
|
async def rebuild(self, session_id):
|
|
admission = getattr(self, "_paperclip_managed_sessions", {}).get(session_id) or _CONTEXT.get()
|
|
if admission is None:
|
|
return await original_rebuild(self, session_id)
|
|
token = _CONTEXT.set(admission)
|
|
try:
|
|
result = await original_rebuild(self, session_id)
|
|
_receipt(self, session_id, admission)
|
|
return result
|
|
finally:
|
|
_CONTEXT.reset(token)
|
|
|
|
@functools.wraps(original_replace)
|
|
async def replace(self, session_id):
|
|
if session_id in getattr(self, "_paperclip_managed_sessions", {}):
|
|
# Never let the native enterprise poller substitute a model on this path.
|
|
return
|
|
return await original_replace(self, session_id)
|
|
|
|
@functools.wraps(original_prompt)
|
|
async def prompt(self, *args, **kwargs):
|
|
session_id = kwargs.get("session_id", args[1] if len(args) > 1 else None)
|
|
admission = getattr(self, "_paperclip_managed_sessions", {}).get(session_id)
|
|
if admission is not None:
|
|
_receipt(self, session_id, admission)
|
|
return await original_prompt(self, *args, **kwargs)
|
|
|
|
def wrap_setting(original):
|
|
@functools.wraps(original)
|
|
async def setting(self, *args, **kwargs):
|
|
session_id = kwargs.get("session_id", args[1] if len(args) > 1 else None)
|
|
managed = getattr(self, "_paperclip_managed_sessions", {})
|
|
if session_id not in managed:
|
|
return await original(self, *args, **kwargs)
|
|
admission = _read_request(kwargs)
|
|
if admission is None or admission["nonce"] == managed[session_id]["nonce"]:
|
|
_fail("managed configuration change requires a fresh admission request")
|
|
session = self._sessions[session_id]
|
|
if not session.agent.conversation.is_idle:
|
|
_fail("cannot change managed configuration during an active turn")
|
|
token = _CONTEXT.set(admission)
|
|
try:
|
|
result = await original(self, *args, **kwargs)
|
|
session = self._sessions[session_id]
|
|
if session.current_model_id != admission["model"]:
|
|
_fail("configuration selected a different model")
|
|
session.system_prompt = admission["content"]
|
|
managed[session_id] = admission
|
|
await rebuild(self, session_id)
|
|
return _add_receipt(result, _receipt(self, session_id, admission))
|
|
finally:
|
|
_CONTEXT.reset(token)
|
|
return setting
|
|
|
|
@functools.wraps(original_cleanup)
|
|
async def cleanup(self, session_id=None):
|
|
try:
|
|
return await original_cleanup(self, session_id)
|
|
finally:
|
|
managed = getattr(self, "_paperclip_managed_sessions", {})
|
|
if session_id is None:
|
|
managed.clear()
|
|
else:
|
|
managed.pop(session_id, None)
|
|
|
|
async def models(self, params):
|
|
if (
|
|
not isinstance(params, dict)
|
|
or set(params) != {"schema", "nonce", "authMethod"}
|
|
or params["schema"] != MODELS_SCHEMA
|
|
or params["authMethod"] != "oauth-personal"
|
|
or not isinstance(params["nonce"], str)
|
|
or not 1 <= len(params["nonce"]) <= 512
|
|
):
|
|
_fail("invalid model catalog request")
|
|
|
|
def session_free():
|
|
if (
|
|
getattr(self, "_sessions", {})
|
|
or getattr(self, "_paperclip_managed_sessions", {})
|
|
or _CONTEXT.get() is not None
|
|
):
|
|
_fail("model catalog requires a session-free connection")
|
|
|
|
def require_personal():
|
|
state = self._ensure_auth_state()
|
|
if state is None or state.method_id != "oauth-personal":
|
|
raise exceptions.RequestError(
|
|
code=-32000,
|
|
message="Google personal sign-in is required before model discovery",
|
|
)
|
|
|
|
session_free()
|
|
try:
|
|
require_personal()
|
|
# Set the persistent instance policy BEFORE the native auth assertion:
|
|
# both assertion and catalog token reads may otherwise open a browser.
|
|
manager = self._ensure_oauth_mgr()
|
|
manager._paperclip_noninteractive = True
|
|
await self._assert_authenticated()
|
|
require_personal()
|
|
except Exception:
|
|
raise exceptions.RequestError(
|
|
code=-32000,
|
|
message="Google personal sign-in must be renewed through Paperclip before model discovery",
|
|
) from None
|
|
session_free()
|
|
try:
|
|
state = await asyncio.to_thread(self._list_available_models, "")
|
|
require_personal()
|
|
values = [
|
|
{"id": model.model_id, "label": model.name}
|
|
for model in state.available_models
|
|
]
|
|
ids = [model["id"] for model in values]
|
|
if (
|
|
not values
|
|
or any(not isinstance(model["id"], str) or not model["id"]
|
|
or not isinstance(model["label"], str) or not model["label"]
|
|
for model in values)
|
|
or len(set(ids)) != len(ids)
|
|
or state.current_model_id not in ids
|
|
):
|
|
raise ValueError("invalid native model catalog")
|
|
except Exception:
|
|
raise exceptions.RequestError(
|
|
code=-32000,
|
|
message="Google Antigravity could not list models for this connection",
|
|
) from None
|
|
session_free()
|
|
return {
|
|
"schema": MODELS_SCHEMA,
|
|
"nonce": params["nonce"],
|
|
"authMethod": "oauth-personal",
|
|
"models": values,
|
|
"defaultModelId": state.current_model_id,
|
|
}
|
|
|
|
async def ext_method(self, method, params):
|
|
if method.lstrip("_") == "paperclip/models":
|
|
return await models(self, params)
|
|
if method.lstrip("_") != "paperclip/admission":
|
|
if original_ext is None:
|
|
_fail("unknown extension method")
|
|
return await original_ext(self, method, params)
|
|
if not isinstance(params, dict) or set(params) != {"sessionId", "nonce"}:
|
|
_fail("invalid admission status request")
|
|
admission = getattr(self, "_paperclip_managed_sessions", {}).get(params["sessionId"])
|
|
if admission is None or admission["nonce"] != params["nonce"]:
|
|
_fail("unknown admission session or nonce")
|
|
return {"paperclipAntigravityAdmission": _receipt(self, params["sessionId"], admission)}
|
|
|
|
adapter_type.initialize = initialize
|
|
if original_oauth_mgr is not None:
|
|
adapter_type._ensure_oauth_mgr = ensure_oauth_mgr
|
|
acp_hooks.load_hooks_config = hooks_config
|
|
acp_hooks.load_hooks_config_async = hooks_config_async
|
|
acp_hooks.has_workspace_hooks = has_workspace_hooks
|
|
system_prompt.extract_system_prompt = extract
|
|
adapter_type._list_available_models = list_models
|
|
adapter_type._create_agent_config = create_config
|
|
adapter_type.new_session = wrap_open(adapter_type.new_session, True)
|
|
adapter_type.load_session = wrap_open(adapter_type.load_session, False)
|
|
adapter_type.resume_session = wrap_open(adapter_type.resume_session, False)
|
|
adapter_type._rebuild_session_agent = rebuild
|
|
adapter_type._maybe_replace_unavailable_model = replace
|
|
adapter_type.prompt = prompt
|
|
adapter_type.cleanup = cleanup
|
|
adapter_type.set_session_model = wrap_setting(adapter_type.set_session_model)
|
|
adapter_type.set_config_option = wrap_setting(adapter_type.set_config_option)
|
|
adapter_type.ext_method = ext_method
|
|
adapter_type._paperclip_admission_installed = True
|