Repository navigation
feat(extras): run a job in AgentCore Runtime - #1217
Draft
ParidelPooya wants to merge 11 commits into
Draft
ParidelPooya wants to merge 11 commits into
ParidelPooya wants to merge 11 commits into
Conversation
Add agentcore() in the compute/agentcore subpath of the extras package. It runs one job in a new Amazon Bedrock AgentCore Runtime session. A step generates the runtime session ID. A request step sends the job document with InvokeAgentRuntime. The function waits on a durable callback. A stop step calls StopRuntimeSession after success, failure, and timeout. Add startAgentcoreWorker() in the agentcore subpath of the compute-worker package. It answers GET /ping and POST /invocations on port 8080. It runs each callback ID once, sends heartbeats, and reports the result with the existing callback reporter. @aws-sdk/client-bedrock-agentcore is an optional peer dependency of the extras package. The worker adds no dependency.
The AgentCore documentation makes time_of_last_update optional and asks to set it only when the status changes. A public report states that the service ends a HealthyBusy session at its idle timeout when the field is missing. So the worker now records the Unix time, in seconds, when it starts and when the status changes between Healthy and HealthyBusy, and sends that time in every ping answer.
2 of 3 tasks
- Bind 0.0.0.0 only when AGENTCORE_RUNTIME_URL is set, else 127.0.0.1. An explicit host still wins. /invocations has no authentication, so a local run stays on loopback. - Start the job on a later turn of the event loop. So synchronous work in the handler cannot delay the accepted answer. The live test showed AgentCore failing such a request with "Runtime initialization time exceeded". - A createClient that throws is logged, as in the MicroVM worker, and no longer ends the process. Heartbeats that fail to start fail the callback. - Docs: the function role also needs runtime-endpoint/* for both actions; explain idle-timeout behavior; offload CPU-bound work. - Tests for the default host and port, the query-string strip, the GET-only /ping, and the 256-character quote of a refused answer. - Replace the unverified note on StopRuntimeSession 404 with the measured behavior.
hln33
reviewed
Nov 12, 2026
A test now passes a custom retryStrategy to agentcore. One case stops the retry of a throttled request, and the operation fails with AgentcoreDeliveryError after one invoke. The other case retries a throttled stop, so the stop runs twice. A worker test returns a result that JSON.stringify cannot encode. The worker reports it as a ResultSerializationError failure. Both tests fail when the branch they cover is removed.
yaythomas
requested changes
Nov 13, 2026
- InvokeAgentRuntime and StopRuntimeSession stop 10 seconds before the Lambda invocation times out, like the MicroVM calls. The helpers move to compute/deadline.ts, which imports no AWS SDK client. - The request step reads at most 64 KiB of the agent's answer, and cancels the stream past it. - The fixed 8-hour timeout cap and MAX_AGENTCORE_JOB_SECONDS are removed. The timeout must fit the agent runtime's maxLifetime. - A heartbeatTimeout below 1 second, or one that is not finite, fails before any durable operation. - The worker wraps its logger like the MicroVM worker, and ends each background chain with a catch. - The worker passes the runtime session ID to the handler as sessionId, and logs it as runtimeSessionId. - The docs state that delivery is at least once across process replacement, so handler side effects must be idempotent. - A cloud-only AgentCore example runs when AGENT_RUNTIME_ARN is set.
The SDK does not guard developer configuration. So the round 2 guards for settings that a developer controls become documented rules. - Remove the 64 KiB answer cap. The request step reads the answer with transformToString() again. The invocation reserve stays. - Remove the heartbeatTimeout check. The docs state the rule. - Remove the worker's logger wrapper. One terminal catch on the job chain stays, so a rejection is never unhandled. - Revert the integration script. The AgentCore example is localOnly until the CI test account has an agent runtime. - Add "Rules for AgentCore jobs" to both READMEs.
main now lists only breaking changes and deprecations in CHANGELOG.md. The AgentCore operation is neither, so the merge keeps the main version and drops the Added entry from this branch.
yaythomas
requested changes
Nov 13, 2026
- The AgentCore worker wraps its logger with the MicroVM worker's safeLogger again. "job started" is logged inside the block whose finally stops the heartbeats. - A result that cannot be sent is reported as a failure, and the worker then logs "job failed", not "job succeeded". - The request and stop steps reuse the MicroVM preflight check, moved to compute/deadline.ts. With less than 11 seconds left, they make no call and say to raise the function timeout. - The stop failure log names the stop permission only for AccessDeniedException, and names maxLifetime for a running handler. - The heartbeatTimeout rule says that the first period must cover delivery and a cold session start.
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.
What
This PR adds a compute operation for Amazon Bedrock AgentCore Runtime. It is the first iteration: one job per call.
agentcore(context, name, input, config)in@aws/durable-execution-sdk-js-extras/compute/agentcore.startAgentcoreWorker({ handler, logger?, createClient? }, port?, host?)in@aws/durable-execution-sdk-js-compute-worker/agentcore.Both subpaths are experimental, like the MicroVM subpaths.
Twin PR: aws/aws-durable-execution-sdk-python#929
Changes since review (round 3)
Commit
c26dc289answers the third review round:safeLogger, which now is exported inside the package. A logger method that throws, or that returns a rejected promise, cannot end the process or skip the job's cleanup."job started"is logged inside the block whosefinallystops the heartbeats. This reverses the round 2 decision to rely on a documented rule: a throwing logger left heartbeats running, which is a worker defect.ResultTooLargeError,ResultSerializationError) is reported as a callback failure. The worker then logs"job failed"and returns. It no longer logs"job succeeded".compute/microvm/request.tstotooLittleTimeMessageincompute/deadline.ts, so both compute types share it, and the MicroVM message is unchanged. With less than 11 seconds left in the invocation, each step makes no call and fails with a retryableTimeoutErrorwhose message says to raise the function timeout.StopRuntimeSessionpermission only when the cause isAccessDeniedException. It says that a session ends at the idle timeout, or, while the handler still runs, at the runtime'smaxLifetime.heartbeatTimeoutTSDoc now say that the first heartbeat period starts before delivery. So it must cover the request retries and a cold session start, measured at 1.4 to 3.1 seconds. The rule suggests 30 seconds or more. A new rule says that the function timeout must be at least 11 seconds.localOnlyuntil the CI test account has an agent runtime. That needs a CDK change, which is the next step. Until it lands, this PR asks a maintainer to record an exception to the integration-test rule of TESTING.md for this example.Changes since review (round 2)
Rule for this round: the SDK does not guard developer configuration. Code stays only where the SDK breaks with a correct setup. Each other case is a rule in the docs that names the setting.
Commit
ca38021aanswered the second review round. Commit415962dfreduced it to the code below.Code:
TimeoutError, and the step retries in a later invocation. The answer is read withtransformToString(), and the abort signal covers that read.compute/microvm/request.tsandshared.tstocompute/deadline.ts. Both MicroVM modules import@aws-sdk/client-lambda-microvms. So an import from them would load that client in thecompute/agentcoresubpath, andpackage-dependencies.test.tswould fail.deadline.tsimports no AWS SDK client. It holds only the reserve constants,reserveStartsAt,callBeforeDeadline, andremainingTime. The MicroVM behavior does not change.timeoutmust be at least 1 second. Removed export:MAX_AGENTCORE_JOB_SECONDSfromcompute/agentcore. The subpath is experimental and unreleased..catch. So a rejection in the job chain is never unhandled.sessionId, read from theX-Amzn-Bedrock-AgentCore-Runtime-Session-Idrequest header. The worker's job log lines carry it asruntimeSessionId, the field name of the Python worker.src/examples/agentcore/run-job/, with a history file. The agent side isagentcore-agent/app.ts. The example haslocalOnly: trueuntil the CI infrastructure exists. So it has no SAM template entry and no name-map entry, and its test does not run. The integration script is unchanged.Rules in the docs, in a "Rules for AgentCore jobs" section of both READMEs:
heartbeatTimeoutto at least 1 second. It ends the job when the worker crashes or stops reporting. TheheartbeatTimeoutTSDoc says the same. The round 2 check for it is removed.timeoutmust fit the agent runtime's configuredmaxLifetime.agentcore()call runs one job, so it does not lose state between jobs.Also removed from round 2: the 64 KiB answer cap. The worker answers
{"accepted": true}, so a correct setup never sends a long answer.Dependency: CI infrastructure
The AgentCore example runs in CI only after these exist. This PR does not create them, and it changes no CI file.
packages/aws-durable-execution-sdk-js-examples/agentcore-agent/app.ts(a direct code deployment withNODE_22works). The header comment of that file lists the steps.lambda:SendDurableExecutionCallbackSuccess,lambda:SendDurableExecutionCallbackFailure, andlambda:SendDurableExecutionCallbackHeartbeaton the example functions.TEST_LAMBDA_EXECUTION_ROLE_ARNallowsbedrock-agentcore:InvokeAgentRuntimeandbedrock-agentcore:StopRuntimeSessionon the agent runtime ARN and on<runtime ARN>/runtime-endpoint/*.AGENT_RUNTIME_ARN. This needs a change to the SAM template generator and the integration script.localOnly: trueis removed.Why
A developer deploys an agent runtime outside the durable function. The function needs to send the agent a job and get the result back. The function should not pay for Lambda compute while the agent works. The MicroVM operations solve the same problem for AWS Lambda MicroVMs. AgentCore Runtime fits the same model:
SendDurableExecutionCallbackSuccess,SendDurableExecutionCallbackFailure, andSendDurableExecutionCallbackHeartbeat.Design
This section is the design note that CONTRIBUTING rule 3 asks for.
Problem. A durable function has no built-in way to run a long job in an AgentCore Runtime agent and wait for its result without keeping the invocation running.
Cause. InvokeAgentRuntime is a request and response call, and one request lasts at most 15 minutes. A job that runs longer needs another way to return its result.
Options considered.
compute/agentcore, built likecompute/microvm. The operation sends the job with InvokeAgentRuntime. The agent answers at once and reports the result through a durable callback. A worker in the compute-worker package implements the agent side.createCallback, a step with InvokeAgentRuntime, a stop step, and the agent side by hand. That is about 50 lines in the function, and the agent side needs dedup, heartbeats, and the ping contract. Each developer would rebuild the same code.Chosen option: 1. It matches the existing compute model and the existing worker package. The compute is defined outside the function, so the same shape supports more compute types later.
One job per call in this iteration. A session with several jobs (
agentcoreSession) needs a decision about idle sessions. AgentCore Runtime has no suspend. A session that receives no request for itsidleRuntimeSessionTimeoutends and loses its memory and files. A durable wait between two jobs can be longer than that timeout. So a session operation needs a live probe of the idle behavior first. One job per call has no such gap: the worker reportsHealthyBusywhile the job runs.What the change removes. Nothing.
Change
Extras package:
src/compute/agentcore/agentcore.ts: theagentcoreoperation, input encoding and size check, the stop step.src/compute/agentcore/errors.ts:AgentcoreError extends ComputeError, withAgentcoreDeliveryError,AgentcoreJobFailedError, andAgentcoreTimeoutError. The error mapper restores the class from the checkpoint.src/compute/agentcore/subtypes.ts:AgentcoreOperationSubType.src/compute/agentcore/types.ts:AgentcoreConfigandAgentcoreJobDocument.package.jsonandrollup.config.mjs: the./compute/agentcoresubpath, and@aws-sdk/client-bedrock-agentcoreas an optional peer dependency.Compute-worker package:
src/agentcore/worker.ts:startAgentcoreWorker.src/microvm/worker.ts:readJsonis now exported for use inside the package. Themicrovmsubpath does not export it. No behavior changes.package.jsonandrollup.config.mjs: the./agentcoresubpath.Docs: one README section per package ("Run a job in AgentCore Runtime"), and a CHANGELOG entry.
Behavior
The operation creates its durable operations inside one child context named
name, with subtypeAgentcore:<name>.session(step,AgentcoreSessionId) generates the runtime session ID, a UUID of 36 characters. The SDK checkpoints it. So every retry and every replay uses the same session.<name>.callback(callback,AgentcoreCallback) hastimeoutandheartbeatTimeout.<name>.request(step,AgentcoreRequest) calls InvokeAgentRuntime withagentRuntimeArn,runtimeSessionId,qualifierwhen set,contentType: application/json, and the job document{version: 1, region, callbackId, heartbeatTimeoutSeconds?, input}. The step reads the streamed response to the end. An answer other than{"accepted": true}fails the step without a retry. The call and the read stop 10 seconds before the invocation times out, and the step retries in a later invocation.<name>.stop(step,AgentcoreStop) calls StopRuntimeSession, which also stops 10 seconds before the invocation times out. It runs in afinallyblock. So it runs after success, after a job failure, after a timeout, and after a failed request. A stop failure is logged as an error and does not replace the job's outcome, as in MicroVM terminate.Dedup rule. InvokeAgentRuntime has no idempotency token. So a retried request step can deliver the same job twice to the same session. The worker remembers each callback ID that it accepted, up to the last 1,000. A repeated callback ID gets
{"accepted": true}and does not run again. The durable callback accepts only one success. So a duplicate run could never change the result, but it could repeat side effects. So the dedup is in the worker. The worker keeps the IDs in memory. A retry that reaches a new process, for example after the idle timeout, runs the job again. So delivery is at least once, and handler side effects must be idempotent.Worker contract.
0.0.0.0when theAGENTCORE_RUNTIME_URLenvironment variable is set, and127.0.0.1otherwise. An explicithostargument wins. AgentCore Runtime requires0.0.0.0:8080, and the live test showed that it setsAGENTCORE_RUNTIME_URLalso for a direct code deployment, which has no/.dockerenvand noDOCKER_CONTAINER./invocationshas no authentication of its own. So a run outside AgentCore stays on loopback.GET /pinganswers{"status": "HealthyBusy"}while a job runs, and{"status": "Healthy"}otherwise. The answer also hastime_of_last_update: the Unix time, in seconds, of the last status change. The worker sets it when it starts and when the status changes, not on every ping.POST /invocationsvalidates the document with the existingparseJobRequestand answers{"accepted": true}. The job starts on a later turn of the event loop (setImmediate), after the answer is written. So synchronous work at the start of a handler cannot delay the answer. An invalid document gets HTTP 400.createClientthat throws is logged as an error, as in the MicroVM worker, and the process keeps serving. Heartbeats that fail to start fail the callback, and the handler does not run./pingand sends heartbeats. Synchronous CPU work blocks both. The README and the TSDoc say to run CPU-bound work in a worker thread or a child process.sessionIdfrom theX-Amzn-Bedrock-AgentCore-Runtime-Session-Idrequest header. The job log lines carry it asruntimeSessionId.safeLogger. The background job chain ends with a.catch, so a rejection there is never unhandled.CallbackReporterandstartHeartbeatsof the package.Idle timeout. After the agent runtime's
idleRuntimeSessionTimeout, the same session ID silently gets a new microVM, and memory and files are lost (measured in the live test). The operation sends one job per session. So no later job can reach the new microVM. The idle timeout matters only while the job runs, and the worker then reportsHealthyBusy, which keeps the session alive.Permissions
bedrock-agentcore:InvokeAgentRuntimeandbedrock-agentcore:StopRuntimeSessionon both the agent runtime ARN and<runtime ARN>/runtime-endpoint/*. The live test showed a denial on<runtime ARN>/runtime-endpoint/DEFAULTwithout the endpoint resource.lambda:SendDurableExecutionCallbackSuccess,lambda:SendDurableExecutionCallbackFailure, andlambda:SendDurableExecutionCallbackHeartbeaton the function ARN.Decisions the brief did not cover
CONTRIBUTING rule 7 asks for this list. Each line is one decision and its reason.
regionfield. The agent runtime can be in another Region than the function. The callback must go to the function's Region. The MicroVM job request has the same field.AWS_REGIONonly. Lambda sets it to the function's Region. The MicroVM code also reads the execution ARN. That second source adds a branch with no case that needs it.defaultMicrovmRetryStrategy, imported inside the package. It retries throttling, server errors, connection failures, and errors that the AWS SDK marks retryable, such asRetryableConflictException. No new exported strategy.timeoutmust be at least 1 second. The runtime'smaxLifetimelimits the session, but the ARN does not say which limit applies (8 hours for microVM, up to 14 days for Instances). So the operation does not cap it.BedrockAgentCoreClientper Region of the agent runtime ARN.ResourceNotFoundExceptionfrom StopRuntimeSession counts as an ended session, not as a failure. The live test measured it: 404 for a session that a stop already ended, and for a session ID that never started./invocations, not 202. The AgentCore contract describes a 200 answer.RuntimeClientError(HTTP 424), which the default strategy does not retry. So the request step fails withAgentcoreDeliveryErrorat once.startAgentcoreWorkerhas a third parameter,host. Its default followsAGENTCORE_RUNTIME_URL, as described under "Worker contract".durationToSecondsis copied into the agentcore module. Importing it fromcompute/microvm/sharedwould load@aws-sdk/client-lambda-microvms, which an AgentCore user may not install. A test checks that each subpath loads without the client of the other subpath.TimeoutError. The default retry strategy already retries that name. So no new error class and no change to the strategy.localOnlyflag to stay out of CI, and no new catalog flag. A new flag would need SAM template and integration script changes that nothing uses until the CI infrastructure exists.npm installraised the lockfile versions of some shared@aws-sdk/*and@smithy/*packages, because@aws-sdk/client-bedrock-agentcoreneeds them.Changed test expectations
src/__tests__/package-dependencies.test.tsin the extras package lists each AWS SDK client and its subpath. It has a new entry for@aws-sdk/client-bedrock-agentcore, and a new check that each subpath loads without the client of another subpath. No other existing test changed.Out of scope / follow-ups
agentcoreSession: several jobs in one session. The live test showed that an idle session silently gets a new microVM on the same ID, so a session operation must handle that.startHeartbeats,readJson,parseJobRequest) to the top level of the compute-worker package. Today theagentcoresubpath imports them fromsrc/microvm/. So it also loads@aws-sdk/client-lambda-microvms, which is a regular dependency of the package.Testing
Commands:
npm ci, rootnpm run build,npm testin both packages,npm run lint:ci,npm run lint:suppressions,npm run lint:versions,npm run lint:filenames. Node 24.Round 3, at
c26dc289: extras 29 test files and 471 tests pass; compute worker 6 test files and 336 tests pass; examples 127 test files and 277 tests pass, with 5 files and 5 tests skipped. Biome passes. Each new branch was removed once, and a test failed each time: thesafeLoggerwrapper, thereturnafter a result that cannot be sent, the request preflight, the stop preflight, theAccessDeniedExceptioncondition of the permission hint, and themaxLifetimetext. Moving"job started"back before thetryfails no test whilesafeLoggeris in place, because the wrapper already drops the throw. The placement keeps the heartbeats stopped if a later change logs through an unwrapped path.Round 2, at
415962df: extras 29 test files and 469 tests pass; compute worker 6 test files and 334 tests pass; examples 127 test files and 277 tests pass, with 5 files and 5 tests skipped (cloud-only and local-only examples, the new AgentCore example among them). Biome passes. Each kept branch was removed once, and a test failed each time: the abort signal on InvokeAgentRuntime, the abort signal on StopRuntimeSession, the removed 8-hour cap, the terminal.catchon the job chain (a test with a logger that throws), the session header read, and theruntimeSessionIdlog field.Round 1:
compute/agentcore/__tests__/agentcore.test.ts, 3 composed tests incompute/agentcore/__tests__/worker-composed.test.ts, and 5 more cases inpackage-dependencies.test.ts. Coverage ofsrc/compute/agentcore: 100% statements and 100% branches.src/agentcore/__tests__/worker.test.tsand 1 insrc/agentcore/__tests__/heartbeat-start.test.ts. Coverage ofsrc/agentcore: 98% statements and 94.11% branches.setImmediatebefore the job, the default host, the default port 8080, the query-string strip, GET-only/ping, thecreateClientcatch, and the 256-character quote of a refused answer.accepted, a throttled request retried with the same session ID, replay with the checkpointed session ID, stop after each failure, a stop failure that keeps the result and the failure, and input and config validation.time_of_last_update(the same across pings without a status change, new when the first job starts and when the last job ends, unchanged when a second job starts or one of two jobs ends), dedup, the 1,000-ID limit, success and failure callbacks, a result over 256 KB, heartbeats, a closed callback, invalid documents with 400, an unknown route with 404,/pingwith a query string and with POST, the default port and host with and withoutAGENTCORE_RUNTIME_URL, an explicit host, the answer written before a handler with synchronous work starts, a throwingcreateClient, and heartbeats that fail to start.agentcoreagainst the real worker server on127.0.0.1. A fake AgentCore client forwards the payload over HTTP. The worker's fake Lambda client completes the callback in the local test runner. They check the result, a failed job, and a payload delivered twice that runs once.importand withrequire. The builtcompute/agentcoreentry does not import@aws-sdk/client-lambda-microvms.Live test
The code ran against a live AgentCore Runtime in us-east-1, with direct code deployment (
NODE_22). The durable function ran theagentcoreoperation, and the agent ranstartAgentcoreWorker. The commit under test wasdea5b490. This revision fixes the one failure found there (4b).AgentcoreJobFailedError. The stop step ran.HealthyBusywithtime_of_last_updateset on change kept the session alive for 3 times the idle timeout.AgentcoreTimeoutError30 s after delivery. The stop step ran.awaitdea5b490. The accept answer never came, and the service failed the request withRuntimeClientError"Runtime initialization time exceeded". Fixed here: the job now starts after the answer is written.{"accepted": true}, and the handler ran once.ResourceNotFoundException. The stop step succeeds and logs no error.Measurements:
{"accepted": true}: 1.4 to 3.1 s.contentType: application/jsonandstatusCode: 200.RuntimeClientErrorwith HTTP 424. The default retry strategy does not retry it. So the request step fails at once withAgentcoreDeliveryError.<runtime ARN>/runtime-endpoint/DEFAULTuntil the function role also allowed<runtime ARN>/runtime-endpoint/*./.dockerenvand noDOCKER_CONTAINER, and it setsAGENTCORE_RUNTIME_URL. So the worker uses that variable to pick its default bind.A second live run, at
1f0e6c15, passed all 6 scenarios, including 4b, and confirmed the bind rule and thecreateClienthandling.Not yet run live: the round 2 changes. Unverified against the real service: that an abort of the AWS SDK call also ends a
transformToString()read of a response stream that stalls after its headers. A unit test with a fake covers a stalled call. TheX-Amzn-Bedrock-AgentCore-Runtime-Session-Idheader is verified: the Python worker reads it, and the second live run returned the same session ID that the operation generated.