Files
CASAN/packages/casan-harness/adapters/vscode/vscode_hook.py
T

105 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""VS Code/GitHub Copilot adapter for the CASAN-owned `@casan` participant.
The VS Code extension owns the request and native model call, while this thin
adapter maps participant lifecycle messages to the shared Plan-20 bridge. It
does not call a model and it does not claim to intercept built-in Copilot chat.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
_ADAPTER_DIR = os.path.dirname(os.path.abspath(__file__))
_BRIDGE_DIR = os.path.abspath(os.path.join(_ADAPTER_DIR, "..", "..", "scripts", "python"))
if _BRIDGE_DIR not in sys.path:
sys.path.insert(0, _BRIDGE_DIR)
import agentic_bridge as bridge # noqa: E402
ADAPTER_VERSION = "20.2.0-vscode"
def emit(value, exit_code=0):
sys.stdout.write(json.dumps(value, ensure_ascii=False) + "\n")
return exit_code
def handle_begin(payload):
response = bridge.op_begin({
"op": "begin",
"client": "vscode",
"client_version": payload.get("client_version"),
"adapter_version": ADAPTER_VERSION,
"project": payload.get("project") or payload.get("cwd"),
"session": payload.get("session_id"),
"turn": payload.get("turn_id"),
"prompt": payload.get("prompt", ""),
# The explicit @casan participant owns the complete model lifecycle.
"integration_mode": "casan_owned",
})
return emit(response, 2 if response.get("decision") == "block" else 0)
def handle_finalize(payload):
response = bridge.op_finalize({
"op": "finalize",
"admission_id": payload.get("admission_id", ""),
"stop_reason": payload.get("stop_reason") or "completed",
"assistant_summary": payload.get("assistant_summary"),
})
return emit(response)
def handle_abort(payload):
response = bridge.op_abort({
"op": "abort",
"admission_id": payload.get("admission_id", ""),
"reason": payload.get("reason") or "vscode_participant_aborted",
})
return emit(response)
def handle_telemetry(payload):
response = bridge.op_telemetry({
"op": "telemetry",
"admission_id": payload.get("admission_id", ""),
"model": payload.get("model"),
"runtime_ms": payload.get("runtime_ms"),
"input_tokens": payload.get("input_tokens"),
"output_tokens": payload.get("output_tokens"),
"cost_amount": payload.get("cost_amount"),
"cost_currency": payload.get("cost_currency"),
"cost_source": payload.get("cost_source"),
})
return emit(response)
HANDLERS = {
"Begin": handle_begin,
"Finalize": handle_finalize,
"Abort": handle_abort,
"Telemetry": handle_telemetry,
}
def main(argv=None):
parser = argparse.ArgumentParser(description="CASAN VS Code participant adapter")
parser.add_argument("--event", required=True, choices=sorted(HANDLERS))
args = parser.parse_args(argv)
try:
payload = json.loads(sys.stdin.read() or "{}")
except ValueError:
return emit({"decision": "block", "reason": "invalid_json"}, 2)
try:
return HANDLERS[args.event](payload)
except Exception as exc: # noqa: BLE001 - adapter boundary
return emit({"decision": "block", "reason": "adapter_error:%s" % exc}, 2)
if __name__ == "__main__":
raise SystemExit(main())