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:
@@ -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 **2–3 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"]
|
||||
|
||||
Reference in New Issue
Block a user