#!/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())