diff --git a/01-features/01-harness/01-advanced-examples/13-aws-skills/README.md b/01-features/01-harness/01-advanced-examples/13-aws-skills/README.md index 99587d09..aa48fdf4 100644 --- a/01-features/01-harness/01-advanced-examples/13-aws-skills/README.md +++ b/01-features/01-harness/01-advanced-examples/13-aws-skills/README.md @@ -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. diff --git a/01-features/01-harness/01-advanced-examples/13-aws-skills/aws_skills.py b/01-features/01-harness/01-advanced-examples/13-aws-skills/aws_skills.py index 107e8b33..6bc67796 100644 --- a/01-features/01-harness/01-advanced-examples/13-aws-skills/aws_skills.py +++ b/01-features/01-harness/01-advanced-examples/13-aws-skills/aws_skills.py @@ -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"]