1
0
mirror of synced 2026-08-05 02:57:12 +00:00

fix(harness/13-aws-skills): handle transport and throttling stream errors (#1879)

`stream_response` caught only `EventStreamError`, so a mid-stream transport
failure (`ReadTimeoutError`, `ConnectionClosedError` — both `BotoCoreError`)
or a `ThrottlingException` (a plain `ClientError`) escaped as an unhandled
traceback that named nothing about the cause, aborting the demo even when the
agent's complete answer had already been printed. Catch both base classes,
report the cause, and re-raise only when no content arrived.

Also fix `--raw-events` discarding good responses: raw mode skipped text
accumulation, so `full_text` stayed empty and the "did we receive anything?"
guard re-raised even a cosmetic close error.

Remove the dead `REGION` assignment (never read; the region actually resolves
in `utils/client.py`, which honours `AWS_DEFAULT_REGION` or `AWS_REGION`) along
with the now-unused `import os`, and the entirely unused `import boto3`.
Correct a comment that described a `tools`/`allowedTools` argument the call
does not pass.

README: add a Prerequisites section including the boto3 >= 1.43.32 floor that
`awsSkills` requires, document `--role-arn`/`--raw-events`/`--skip-cleanup`
and the ~2-3 minute wait for READY, and state the tagged-union and
wildcard semantics of `skills` entries.
This commit is contained in:
Rui Cardoso
2026-08-03 13:17:04 +01:00
committed by GitHub
parent 34e039f36a
commit 2ac3ce2b35
2 changed files with 55 additions and 11 deletions
@@ -53,6 +53,25 @@ skills=[
set on the harness resource (so they apply to every invocation) or passed per
`invoke_harness` call.
Each `skills` entry is a *tagged union*, so set exactly one of `path`, `s3`, `git`
or `awsSkills` per entry — combining two in one entry is rejected before the call
is sent. To use several sources, add one entry each, as `--mode mixed` does.
`paths` accepts `*` as a wildcard; `?` and character classes such as `[abc]` are
not part of the accepted pattern.
## Prerequisites
- **AWS credentials** for a region where AgentCore Harness is available, and
**model access** to `global.anthropic.claude-haiku-4-5-20251001-v1:0` (or pass
another model with `--model`).
- **boto3 ≥ 1.43.32** — the first release whose `bedrock-agentcore` model knows
the `awsSkills` skill source. On anything older this sample fails with
`ParamValidationError: Unknown parameter in skills[0]: "awsSkills"`.
`../../requirements.txt` already pins this floor.
- The script creates the shared `HarnessExecutionRole` and deletes it again on
exit, unless you pass `--role-arn` (a role you supply is never deleted).
## Sample Prompts
**Prompt** (`--mode glob`, default): "What AWS skills do you have available? Give a short bulleted summary by category."
@@ -99,4 +118,17 @@ python aws_skills.py --mode specific \
# Combine serverless + CDK skills with a build task
python aws_skills.py --mode mixed \
-m "Design a Step Functions state machine for order processing and outline the CDK stack."
# Reuse an existing execution role (it is then left in place on cleanup)
python aws_skills.py --role-arn arn:aws:iam::111122223333:role/MyHarnessRole
# Print the raw streaming events instead of just the assistant text
python aws_skills.py --raw-events
# Keep the harness after the demo
python aws_skills.py --skip-cleanup
```
A harness that enables AWS Skills takes roughly **23 minutes** to reach `READY`
while the runtime loads them, so expect a wait on `Step 1` before the agent
replies. `python aws_skills.py --help` lists every option.
@@ -59,14 +59,12 @@ Usage:
import argparse
import json
import os
import sys
import time
import uuid
from pathlib import Path
import boto3
import botocore.exceptions
from botocore.exceptions import BotoCoreError, ClientError
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
@@ -74,9 +72,6 @@ from utils.client import get_agentcore_client, get_agentcore_control_client
from utils.harness import poll_harness_status
from utils.iam import create_harness_role, delete_harness_role
REGION = os.getenv("AWS_DEFAULT_REGION")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
@@ -174,10 +169,24 @@ def stream_response(client, harness_arn, session_id, message, model_id, raw=Fals
)
full_text = ""
# A failure mid-stream is raised out of the iterator by botocore, not
# delivered as an {"internalServerException": ...} event. It can be a modeled
# service error (EventStreamError/ClientError — throttling, access denied) or
# a transport error (ReadTimeoutError, connection reset — a BotoCoreError,
# which the agent can trigger just by going quiet during a long tool call).
# Catch both base classes: EventStreamError alone let a read timeout or a
# throttle abort the script before the `finally` in main() had reported the
# partial answer, and the traceback said nothing about the real cause.
try:
for event in response["stream"]:
if raw:
print(json.dumps(event, default=str))
# Keep accumulating text in raw mode too. Skipping it left
# full_text empty, so the error handling below could not tell a
# stream that had produced content from one that had not.
delta = event.get("contentBlockDelta", {}).get("delta", {})
if "text" in delta:
full_text += delta["text"]
continue
if "contentBlockStart" in event:
@@ -193,9 +202,11 @@ def stream_response(client, harness_arn, session_id, message, model_id, raw=Fals
print()
elif "internalServerException" in event:
print(f"\n Error: {event['internalServerException']}")
except botocore.exceptions.EventStreamError:
# The stream may send an empty error event on close; safe to ignore
# if we already received content.
except (BotoCoreError, ClientError) as e:
# The stream may fail on close after delivering the whole answer. Report
# it either way, but only re-raise when nothing arrived — otherwise a
# cosmetic close error would throw away a complete, correct response.
print(f"\n Stream error: {e}")
if not full_text:
raise
@@ -240,8 +251,9 @@ def main(args=None):
harnessName=harness_name,
executionRoleArn=role_arn,
skills=skills,
# Smaller models benefit most from skills; allow the agent to use
# its default tools (fs/shell) so it can act on what the skill teaches.
# No `tools`/`allowedTools` is set, so the harness keeps its built-in
# toolset (fs/shell) — that is what lets the agent read the skill
# files it was given and act on what they teach.
systemPrompt=[{"text": "You are a helpful AWS engineering assistant."}],
)
harness_id = resp["harness"]["harnessId"]