Repository navigation
feat(extras): run a job in AgentCore Runtime - #929
Draft
ParidelPooya wants to merge 12 commits into
Draft
ParidelPooya wants to merge 12 commits into
ParidelPooya wants to merge 12 commits into
Conversation
Add the experimental agentcore operation to the extras package. It runs one job in a new Amazon Bedrock AgentCore Runtime session, waits on a durable callback, and stops the session. Add the agentcore worker to the compute-worker package. It serves POST /invocations and GET /ping in the agent's container. It reuses the callback reporter, the heartbeats, and the job document parser of the MicroVM worker.
Apply the review and live-test fixes to the AgentCore operation and worker. - The worker listens on 0.0.0.0 only when AGENTCORE_RUNTIME_URL is set, and on 127.0.0.1 otherwise. An explicit host still wins. - A job whose heartbeats cannot start fails its callback. - The worker listens with a backlog of 511, as the MicroVM worker. - The docs list the runtime-endpoint resource in the function role, and describe the idle-timeout behavior that the live test measured. - The comments on stop and on a worker 500 match the live behavior. - New tests cover the default host and port, the backlog, the dedup lock, and the 64 KiB read limit on the answer.
yaythomas
requested changes
Nov 13, 2026
A Lambda timeout during a step attempt records no outcome for the attempt. The default client can spend about 3 minutes on one call. So a stalled InvokeAgentRuntime or StopRuntimeSession call could run until Lambda ended the invocation, and replay would start it again. Both calls now run through call_before_deadline with the remaining Lambda time, as the MicroVM calls do. A call that has not ended 10 seconds before the invocation times out fails its attempt with a retryable TimeoutError. So the step records an outcome, and a stalled stop no longer hides a recorded result.
The worker registered the callback ID before Thread.start() and outside the lock. So a duplicate request during a blocked start got HTTP 200, and the job never ran when the start then failed. The new thread could also run the handler before the HTTP 200 was written. So a handler that held the GIL delayed the answer. Registration and Thread.start() now run as one step under the lock. A failed start forgets the callback ID and answers 500, and a duplicate waits for the lock. The job thread waits for an event that is set after the answer is written, also when the write fails.
The agentcore operations appeared in history as plain RunInChildContext, Step, and Callback entries. The JavaScript operation records five AgentCore subtypes. So history and plugin telemetry differed between the two SDKs. AgentcoreOperationSubType defines the same five values as JavaScript. The operation passes them through ChildConfig, StepConfig, and CallbackConfig. Custom subtypes arrived in core SDK 3.0.0. So the extras package now requires aws-durable-execution-sdk-python 3.0.0 or later.
The worker remembers callback IDs only in process memory. A retry can arrive after the idle timeout replaced the session's process, and the new process runs the same job again. The docs said that a repeated callback ID never runs again. The operation and config docstrings, the worker docstring, and both READMEs now say that dedup holds per process. They say that delivery is at-least-once across process replacement, and that handler side effects must be idempotent.
The agentcore operation crosses AgentCore routing, IAM, the session lifecycle, HTTP forwarding, and callback delivery. The composed unit test replaces those parts with fakes. So no checked-in test covered the real path. The AgentCore Run Job example and its cloud-only test run that path, in the same way that the MicroVM examples do. The catalog flag usesAgentcore gives the function the condition HasAgentRuntime, which holds only when the AgentRuntimeArn parameter is set. The function gets the ARN as AGENT_RUNTIME_ARN. The function-name map leaves the example out unless --include-agentcore is passed. So the test is skipped until CI deploys an agent runtime. agentcore-agent/app.py is the agent.
The previous commit made registration and Thread.start() one locked step, and gated the job thread until the answer was written. That code guarded a failed Thread.start(), which a correct setup does not hit. The worker now writes the HTTP 200 first, and then starts the job's thread. So a handler that holds the GIL still does not delay the answer. A failed thread start is logged, and the job is forgotten. The durable function then fails at its heartbeat timeout. Both READMEs get a "Rules for AgentCore jobs" section. It names the settings that the developer owns: heartbeat_timeout, timeout within maxLifetime, idempotent side effects, a logger that does not throw, and the idle timeout.
The extras README intro on main now names the decider and llm operations. The merged intro names microvm, agentcore, decider and llm.
yaythomas
requested changes
Nov 13, 2026
Thread creation can fail under resource exhaustion in a valid deployment. The worker answered 200 before Thread.start(). So a failed start left the durable function waiting until its heartbeat timeout, or its full timeout without one. The worker now starts the job's thread before it answers. The thread waits on an event before it runs the handler, and the event is set after the 200 is written. So a handler that holds the GIL does not delay the answer. A failed start forgets the callback ID and answers 500, and a later delivery of the same job can run it. Each AgentCore call now runs the MicroVM preflight require_time_for_call. With less than 11 seconds left, the attempt makes no call and asks for a longer function timeout. The stop log names the missing permission only for an AccessDenied answer. The first heartbeat rule now says that heartbeat_timeout must cover the request step and a cold session start. A new rule asks for a function timeout of at least 11 seconds.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Twin PR: aws/aws-durable-execution-sdk-js#1217
Changes since review
These commits address the review from yaythomas:
e803fe3cfix(extras): end AgentCore calls before reserve.InvokeAgentRuntime(with the answer read) andStopRuntimeSessionnow run throughcall_before_deadlinewith the remaining Lambda time, as the MicroVM calls do. A call that has not ended 10 seconds before the invocation times out fails its attempt with a retryableTimeoutError. So a stalled stop no longer hides a recorded result.a23f55b3fix(worker): commit AgentCore job start atomically, simplified in2fda8b46, and reworked in7bae717fafter the second review. The worker starts the job's thread before it answers. The thread waits on an event before it runs the handler, and the event is set after the HTTP 200 is written. So a handler that holds the GIL does not delay the answer. A failedThread.start()forgets the callback ID and answers 500, and a later delivery of the same job can run it. Thread creation can fail under resource exhaustion in a valid deployment, so this is code, not a rule. The lock around the start froma23f55b3is not restored. So a duplicate request that arrives while a start fails can still get 200. In that case the durable function fails at its heartbeat timeout.d816c770feat(extras): record AgentCore operation subtypes.AgentcoreOperationSubTypehas the same five values as the JS twin (Agentcore,AgentcoreSessionId,AgentcoreCallback,AgentcoreRequest,AgentcoreStop). The operation passes them throughChildConfig,StepConfig, andCallbackConfig. Custom subtypes arrived in core SDK 3.0.0. So the extras package now requiresaws-durable-execution-sdk-python>=3.0.0,<4. It required>=2.0.0before. The extras package has no release yet.3f5bb652docs(extras): qualify AgentCore dedup per process. The docstrings and both READMEs now say this: dedup holds per worker process, delivery is at-least-once across process replacement, and handler side effects must be idempotent.7f456339test(examples): add an AgentCore cloud scenario. The examples package gets theAgentCore Run Jobexample, its cloud-only test, and the agentagentcore-agent/app.py. The wiring follows the MicroVM examples, as described in "CI dependency" below.2fda8b46also adds a "Rules for AgentCore jobs" section to the extras and compute-worker READMEs. The SDK does not guard developer configuration. So each rule names the setting that the developer owns.7bae717freworded rule 1: the first heartbeat period starts before the job is delivered, soheartbeat_timeoutmust cover the request step with its retries and a cold session start (1.4 to 3.1 seconds in the live test), and 30 seconds or more is a safe value. It also added rule 6: a function timeout of at least 11 seconds. Theheartbeat_timeoutdocstring has the matching note.7bae717fruns the MicroVM preflightrequire_time_for_callbefore each AgentCore call. With less than 11 seconds left in the invocation, the attempt makes no API call and fails with a message that asks for a longer function timeout. The default retry strategy retries that error, as in MicroVM. The stop failure log names the missing permission only for an AccessDenied answer.Public API added since review:
AgentcoreOperationSubTypein extras.CI dependency
The cloud scenario runs only when CI provides an agent runtime. This PR does not change CI infrastructure or any CI workflow. So the test is skipped in CI until these exist:
packages/aws-durable-execution-sdk-python-examples/agentcore-agent/app.py, with the compute-worker package installed.lambda:SendDurableExecutionCallbackSuccess,lambda:SendDurableExecutionCallbackFailure, andlambda:SendDurableExecutionCallbackHeartbeaton the example function.LambdaExecutionRoleArn):bedrock-agentcore:InvokeAgentRuntimeandbedrock-agentcore:StopRuntimeSessionon the agent runtime ARN and on<runtime ARN>/runtime-endpoint/*.cloud-tests.yml: pass the runtime ARN tosam deployasAgentRuntimeArn, and pass--include-agentcoretogenerate_function_name_map.py.The wiring works like the MicroVM examples:
usesAgentcoregives the function and its log group the conditionHasAgentRuntime. The condition holds only when the template parameterAgentRuntimeArnis set. Its default is empty.AGENT_RUNTIME_ARN.generate_function_name_map.pyleaves the example out unless--include-agentcoreis passed. The conftest skips a test whose example is missing from the map.So a deploy without the parameter leaves the example out of the stack, and the test is skipped.
Issue #, if available
None.
What
This PR adds a first version of a compute operation for Amazon Bedrock AgentCore Runtime. It has two parts.
aws_durable_execution_sdk_python_extras.compute.agentcore. Its operationagentcore(context, name, *, job_input, config)runs one job in a new AgentCore Runtime session and returns the job's result.aws_durable_execution_sdk_python_compute_worker.agentcore. Its entrystart_agentcore_worker(handler=..., logger=None)serves the AgentCore Runtime HTTP contract in the agent's container.Both modules are experimental, like
microvm.Why
AgentCore Runtime runs agent code in an isolated session that the developer deploys outside the durable function. A durable function can send that code a job and wait on a durable callback for the result. The function invocation then ends during the wait, so Lambda bills no compute while the agent works. Today a developer must write that flow and the worker side by hand.
Design
Problem: a durable function cannot run a job on AgentCore Runtime and wait for its result without hand-written code on both sides.
Cause: the SDK has a compute operation only for Lambda MicroVMs.
Options considered:
compute/agentcorethat follows themicrovmflow. Chosen. AgentCore Runtime has the same shape as a MicroVM: the compute is defined outside the function, one request delivers the job, and the agent reports through the durable callback APIs. So the operation reuses the callback flow, the job document, the error base classComputeError, and the worker's callback reporter and heartbeats.InvokeAgentRuntimecall, a session ID, and aStopRuntimeSessioncall at the end. A generic operation would need hooks for each of these. That is more configuration than one compute type.One job per session in this iteration: a session that runs several jobs (
agentcoreSession) needs a rule for waits between jobs. An idle session ends at the agent runtime'sidleRuntimeSessionTimeoutand loses its memory and files. That behavior needs a live probe first. So this PR builds only the one-job operation.This PR removes no code.
Change
Extras (
compute/agentcore/):config.py:AgentcoreConfigwithagent_runtime_arn,timeout,qualifier,heartbeat_timeout,retry_strategy,client.operation.py:agentcore(), andAgentcoreNotAcceptedErrorfor an answer other than{"accepted": true}.errors.py:AgentcoreError(ComputeError),AgentcoreDeliveryError,AgentcoreJobFailedError,AgentcoreTimeoutError. Each class is registered with the SDK error registry, like the MicroVM errors.job.py: the job document{version, region, callbackId, heartbeatTimeoutSeconds?, input}. It is the MicroVM job request withoutmicrovmId.retry.py:default_agentcore_retry_strategy.Worker (
agentcore/worker.py):start_agentcore_worker,AgentcoreWorker,AgentcoreJobContext,AgentcoreJobHandler. It imports the callback reporter, heartbeats, logger, job parser, and the request reader_Exchangefrom themicrovmsubpackage. No module moves in this PR.Other files: README sections in both packages,
boto3-stubs[bedrock-agentcore]in the roottypesanddev-extrasenvironments, and a comment on the boto3 bound of the extras package.Public API members added: 10 in extras (
agentcore,AgentcoreConfig, the 4 error classes,AgentcoreNotAcceptedError,default_agentcore_retry_strategy, and re-exports ofComputeErrorandMAX_JOB_INPUT_BYTES) and 11 in the worker (start_agentcore_worker,AgentcoreWorker,AgentcoreJobContext,AgentcoreJobHandler,default_host,AGENTCORE_HOST,LOCAL_HOST,RUNTIME_URL_ENV,AGENTCORE_PORT,INVOCATIONS_PATH,PING_PATH). Source lines added: about 1,500, half of them docstrings.Behavior
Flow, in one child context named
name:<name>.sessiongenerates the runtime session ID, a UUID of 36 characters.InvokeAgentRuntimerequires at least 33. The step checkpoints the ID. So every retry and replay uses the same session.<name>.callbackcreates the durable callback withtimeoutandheartbeat_timeout.<name>.requestcallsInvokeAgentRuntimewithagentRuntimeArn,runtimeSessionId, the optionalqualifier,contentType: application/json, and the job document aspayload. The step reads at most 64 KiB of the response stream and closes it. An answer other than{"accepted": true}fails the step without a retry, and the operation raisesAgentcoreDeliveryError.<name>.stopcallsStopRuntimeSessionwith the same ARN, session ID, and qualifier.Stop on failure: the stop step runs after success, job failure, timeout, and a failed delivery. A failed stop is logged as an error and does not hide the job's result or error.
ResourceNotFoundExceptionfrom stop counts as stopped. AnExecutionErrorand a retryableInvocationErrorof the wait pass through without a stop, as inmicrovm.Dedup rule:
InvokeAgentRuntimehas no idempotency token. So a retried request step can deliver the same job twice to the same session. The worker remembers every callback ID that it accepted (running, and the last 1,000 finished), in process memory only. It answers a repeated ID with{"accepted": true}and does not run the job again. A retry can arrive after the idle timeout replaced the session's process. The new process then runs the job again. So delivery is at-least-once across process replacement, and handler side effects must be idempotent.Invocation reserve: each
InvokeAgentRuntimeandStopRuntimeSessioncall must end 10 seconds before the Lambda invocation times out. A call that has not ended by then fails its step attempt with a retryableTimeoutError.Subtypes: the child context and the four inner operations record the five
AgentcoreOperationSubTypevalues.Worker:
POST /invocationsparses the job document and checksversion. A document that does not match gets HTTP 400. The worker starts a new job's thread, and then answers{"accepted": true}. The thread runs the handler only after the answer is written. A thread that cannot start gets HTTP 500, and the callback ID is forgotten.GET /pinganswers{"status": "HealthyBusy"}while a job runs or reports, and{"status": "Healthy"}otherwise. The answer always carriestime_of_last_update: the Unix time in seconds of the worker's start or of the last change between the two statuses. It does not change between pings with the same status. The AgentCore docs call the field optional. The issue docs: /ping response requires undocumentedtime_of_last_updatefield — without it AgentCore silently reaps microVMs even when status is HealthyBusy bedrock-agentcore-sdk-python#471 reports that aHealthyBusysession without it can end at the idle timeout.0.0.0.0inside the runtime. The runtime publishes no port, and AgentCore authorizes eachInvokeAgentRuntimecaller with IAM before it forwards the request.AGENTCORE_RUNTIME_URLset in a runtime with direct code deployment, and no/.dockerenvfile orDOCKER_CONTAINERvariable.0.0.0.0whenAGENTCORE_RUNTIME_URLis set, and on127.0.0.1otherwise. An explicithostargument overrides both.Permissions
bedrock-agentcore:InvokeAgentRuntimeandbedrock-agentcore:StopRuntimeSessionon two resources: the agent runtime ARN, and<runtime ARN>/runtime-endpoint/*. The live test denied both calls on<runtime ARN>/runtime-endpoint/DEFAULTwith a policy that named only the runtime ARN. To allow one endpoint only, name the endpoint ofqualifierinstead of*.lambda:SendDurableExecutionCallbackSuccess,lambda:SendDurableExecutionCallbackFailure, andlambda:SendDurableExecutionCallbackHeartbeaton the function ARN.Idle timeout: while a job runs, the worker answers
/pingwithHealthyBusy, so the agent runtime'sidleRuntimeSessionTimeoutdoes not end the job. After the idle timeout, a session with no running job ends. A later request with the same session ID then gets a new microVM without an error, and the memory and files of the old one are lost. This operation sends one job per session, so the loss cannot affect a job.Assumptions
InvokeAgentRuntimeandStopRuntimeSessioncome from the installed botocore 1.44.0 model. A test sends both calls through a real boto3 client withbotocore.stub.Stubber, which validates the parameters against that model. The responseresponsemember is a streaming blob in the model.StopRuntimeSession, checked by installing releases in a venv. The current boundboto3>=1.43.35is newer. So the bound does not change.0.0.0.0, port 8080,/invocations, and/pingcome from the AgentCore Runtime HTTP protocol contract in the AWS docs./invocationsunchanged, with the session ID headerX-Amzn-Bedrock-AgentCore-Runtime-Session-Id. The handler saw the session ID that<name>.sessioncheckpointed.HealthyBusywithtime_of_last_updateset only on a status change kept a session through a 180-second job with an idle timeout of 60 seconds.StopRuntimeSessionanswers 404ResourceNotFoundExceptionfor a stopped or never-started session, and 200 for a session that the idle timeout ended.AGENTCORE_RUNTIME_URLis set in a runtime with direct code deployment. Unverified: that a container deployment sets it too. A container deployment without it binds127.0.0.1and must passhost=AGENTCORE_HOST.Decisions not covered by the issue or the design note
region, as the MicroVM job request does. The agent runtime can run in another Region than the function, and the worker needs the function's Region for the callback calls.timeoutmust be at least 1 second and has no upper bound. A microVM session lives at most 8 hours, but AgentCore Instances sessions can live up to 14 days. So the docstring tells the developer to keep the timeout within the runtime'smaxLifetime.accept: application/json.default_agentcore_retry_strategyretriesThrottlingException,InternalServerException,RetryableConflictException, HTTP 429 and 5xx, and connection errors, with at most 5 attempts.RuntimeClientError(HTTP 424), which is not retried. So the function fails at once withAgentcoreDeliveryError.AGENTCORE_RUNTIME_URL, not from the Docker markers that the AgentCore SDK for Python uses, because the live runtime had no Docker markers.agentcore_prefix, because mypy rejects two test modules with the same name, such asmicrovm/errors_test.pyandagentcore/errors_test.py.Out of scope / follow-ups
agentcore_session: several jobs in one session.compute_worker/microvmto the top level ofcompute_worker.Testing
Since review (Python 3.13, uv venv, the same packages plus the examples package):
7bae717f: 93 passed (was 84). The new ones cover a stalled request that ends before the reserve and is retried, a stalled request that fails the delivery with the reserve error, a stalled stop that does not hide the result, the call without a deadline outside Lambda, the subtype history assertion, the handler starting only after the answer is written, a thread start failure that answers 500 and lets a later delivery run, an invocation with 3 seconds left that makes no call and logs no permission hint, the permission hint for a denied stop, and the retry of the time preflight.7bae717f: 1,359 passed, 7 skipped, 1 failed. The failure ismicrovm/callback_reporter_test.py::test_create_raises_when_boto3_cannot_build_the_client, which fails the same way on the base commit in this environment.hatch fmt --check(hatch 1.16.5): clean in extras, compute-worker, and examples.lintcommit.py: pass for all 7 commits.Before review:
Python 3.13, uv venv with the core, testing, extras, and compute-worker packages installed in editable mode.
pytest -n 4).hatch fmt --checkwith hatch 1.16.5: clean in every package.lintcommit.py: pass.No existing test expectation changed. The extras
conftest.pyonly gains a fakebedrock-agentcoreclient.Live test
The operation and the worker of the first commit ran against live AgentCore Runtime agents in us-east-1, deployed with direct code deployment (
PYTHON_3_13), idle timeout 60 seconds. The JS twin ran the same scenarios.AgentcoreJobFailedError, stop ranAgentcoreTimeoutError30 s after delivery, stop ran{"accepted":true}, the handler ran onceMeasurements:
StopRuntimeSessionon an ended or never-started session answers 404ResourceNotFoundException. The operation treats it as stopped.RuntimeClientError(HTTP 424). The default retry strategy does not retry it.The fix commit (bind default, heartbeat start failure, backlog, docs) and the commits since review have unit tests only. They have not run live.
Checklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.