11"""Invoke Bedrock AgentCore Harness to review a GitHub PR.
22
33Reads PR_URL from the environment. Streams harness output to stdout.
4- Uses raw HTTP with SigV4 signing — no custom service model needed .
4+ Uses the boto3 bedrock-agentcore client's invoke_harness API .
55"""
66
77import json
1111import uuid
1212
1313import boto3
14- from botocore .auth import SigV4Auth
15- from botocore .awsrequest import AWSRequest
16- from botocore .eventstream import EventStreamBuffer
17- from urllib .parse import quote
18- import urllib3
1914
2015# ANSI color codes
2116CYAN = "\033 [36m"
2520DIM = "\033 [2m"
2621RESET = "\033 [0m"
2722
28- SCRIPTS_DIR = os .path .join ( os . path . dirname (__file__ ), ".." )
23+ SCRIPTS_DIR = os .path .dirname (__file__ )
2924
3025
3126def read_prompt (filename ):
@@ -35,50 +30,37 @@ def read_prompt(filename):
3530 return f .read ()
3631
3732
38- def invoke_harness (harness_arn , body , region ):
39- """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response.
40-
41- InvokeHarness is not in standard boto3, so we call the REST API directly.
42- boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.)
43- and sign the request with SigV4. The response is an AWS binary event stream.
44- """
45- session = boto3 .Session (region_name = region )
46- credentials = session .get_credentials ().get_frozen_credentials ()
47- url = f"https://bedrock-agentcore.{ region } .amazonaws.com/harnesses/invoke?harnessArn={ quote (harness_arn , safe = '' )} "
48- request = AWSRequest (method = "POST" , url = url , data = body , headers = {
49- "Content-Type" : "application/json" ,
50- "Accept" : "application/vnd.amazon.eventstream" ,
51- })
52- SigV4Auth (credentials , "bedrock-agentcore" , region ).add_auth (request )
53- return urllib3 .PoolManager ().urlopen (
54- "POST" , url , body = body ,
55- headers = dict (request .headers ),
56- preload_content = False ,
57- timeout = urllib3 .Timeout (connect = 10 , read = 600 ),
33+ def invoke_harness_streaming (harness_arn , session_id , system_prompt , messages , model_id , region ):
34+ """Call invoke_harness via boto3 and return the event stream."""
35+ client = boto3 .client ("bedrock-agentcore" , region_name = region )
36+ response = client .invoke_harness (
37+ harnessArn = harness_arn ,
38+ runtimeSessionId = session_id ,
39+ systemPrompt = [{"text" : system_prompt }],
40+ messages = messages ,
41+ model = {"bedrockModelConfig" : {"modelId" : model_id }},
5842 )
59-
60-
61- def parse_events (http_response ):
62- """Yield (event_type, payload) tuples from the harness binary event stream.
63-
64- The response arrives as raw bytes in AWS binary event stream format.
65- EventStreamBuffer reassembles complete events from the 4KB chunks,
66- and we decode each event's JSON payload before yielding it.
67- """
68- event_buffer = EventStreamBuffer ()
69- for chunk in http_response .stream (4096 ):
70- event_buffer .add_data (chunk )
71- for event in event_buffer :
72- if event .headers .get (":message-type" ) == "exception" :
73- payload = json .loads (event .payload .decode ("utf-8" ))
74- print (f"\n { RED } ERROR: { payload } { RESET } " , file = sys .stderr )
75- sys .exit (1 )
76- event_type = event .headers .get (":event-type" , "" )
77- if event .payload :
78- yield event_type , json .loads (event .payload .decode ("utf-8" ))
79-
80-
81- def print_stream (http_response ):
43+ return response ["stream" ]
44+
45+
46+ def parse_events (event_stream ):
47+ """Yield (event_type, payload) tuples from the boto3 event stream."""
48+ for event in event_stream :
49+ if "contentBlockStart" in event :
50+ yield "contentBlockStart" , event ["contentBlockStart" ]
51+ elif "contentBlockDelta" in event :
52+ yield "contentBlockDelta" , event ["contentBlockDelta" ]
53+ elif "contentBlockStop" in event :
54+ yield "contentBlockStop" , event ["contentBlockStop" ]
55+ elif "messageStop" in event :
56+ yield "messageStop" , event ["messageStop" ]
57+ elif "internalServerException" in event :
58+ yield "internalServerException" , event ["internalServerException" ]
59+ elif "runtimeClientError" in event :
60+ yield "runtimeClientError" , event ["runtimeClientError" ]
61+
62+
63+ def print_stream (event_stream ):
8264 """Display harness events with GitHub Actions log groups.
8365
8466 The harness streams events as the agent works:
@@ -112,7 +94,7 @@ def flush_text():
11294 print (f"{ DIM } { line } { RESET } " , flush = True )
11395 text_buffer = ""
11496
115- for event_type , payload in parse_events (http_response ):
97+ for event_type , payload in parse_events (event_stream ):
11698
11799 if event_type == "contentBlockStart" :
118100 start = payload .get ("start" , {})
@@ -171,6 +153,11 @@ def flush_text():
171153 print (f"\n { RED } ERROR: { payload } { RESET } " , file = sys .stderr )
172154 sys .exit (1 )
173155
156+ elif event_type == "runtimeClientError" :
157+ close_group ()
158+ print (f"\n { RED } ERROR: { payload .get ('message' , payload )} { RESET } " , file = sys .stderr )
159+ sys .exit (1 )
160+
174161 close_group ()
175162 total = time .time () - start_time
176163 print (f"\n { GREEN } Review complete.{ RESET } { DIM } ({ iteration } tool calls, { int (total )} s total){ RESET } " )
@@ -200,18 +187,14 @@ def flush_text():
200187SYSTEM_PROMPT = read_prompt ("system.md" )
201188REVIEW_PROMPT = read_prompt ("review.md" ).format (pr_url = PR_URL )
202189
203- request_body = json .dumps ({
204- "runtimeSessionId" : SESSION_ID ,
205- "systemPrompt" : [{"text" : SYSTEM_PROMPT }],
206- "messages" : [{"role" : "user" , "content" : [{"text" : REVIEW_PROMPT }]}],
207- "model" : {"bedrockModelConfig" : {"modelId" : MODEL_ID }},
208- })
190+ messages = [{"role" : "user" , "content" : [{"text" : REVIEW_PROMPT }]}]
209191
210- http_response = invoke_harness (HARNESS_ARN , request_body , REGION )
211-
212- if http_response .status != 200 :
213- error = http_response .read ().decode ("utf-8" )
214- print (f"{ RED } ERROR: HTTP { http_response .status } : { error } { RESET } " , file = sys .stderr )
192+ try :
193+ event_stream = invoke_harness_streaming (
194+ HARNESS_ARN , SESSION_ID , SYSTEM_PROMPT , messages , MODEL_ID , REGION
195+ )
196+ except Exception as e :
197+ print (f"{ RED } ERROR: Failed to invoke harness: { e } { RESET } " , file = sys .stderr )
215198 sys .exit (1 )
216199
217- print_stream (http_response )
200+ print_stream (event_stream )
0 commit comments