You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add packages/managed-agents-adapter/ to codespar-core. This package bridges the Anthropic Managed Agents SDK to the SessionBase interface defined in @codespar/session-contract (issue #2). Callers typed against SessionBase work unchanged regardless of which runtime backs the session.
This is a pre-GA implementation. The Anthropic Managed Agents API is in beta; the adapter stubs the AgentRuntime type locally and will switch to the official SDK export at GA without changing the public surface.
Context
SessionBase (delivered in issue #2) is a runtime-agnostic interface for agent sessions: execute, send, sendStream, connections, close. This adapter provides the Managed Agents-backed implementation of that interface.
The adapter is an internal package — it does not bump @codespar/session-contract when updated. Its only public export is createManagedAgentsSession plus the five error classes.
Commerce agents call tools with financial consequence (Pix transfers, NF-e issuance). The adapter enforces two non-optional safety properties before any tool execution reaches the Managed Agents API:
Tool name validation — rejects any name that does not match /^[a-zA-Z0-9_-]+$/. Whitespace or newline characters in a tool name would be serialized into the synthesized JSON message and could inject instructions into the runtime.
Policy evaluation order — the PolicyHook runs on the original params before sanitizeParams transforms them. Reversing the order would let a caller strip an amount field and bypass the fund-transfer cap check.
A mutex guards each state-mutating method (execute, send, sendStream). The mutex is reset in a finally block in all three paths. Without the finally, a single rejected operation permanently blocks the mutex and every subsequent call hangs.
DrainTimeoutError means the session state is indeterminate. The adapter documents in JSDoc that callers must not automatically retry commerce tools after this error — the tool call may or may not have executed on the remote side.
Acceptance Criteria
Package scaffold
packages/managed-agents-adapter/package.json sets name: "@codespar/managed-agents-adapter", version: "0.1.0", and lists @codespar/session-contract as a peer dependency.
packages/managed-agents-adapter/tsconfig.json extends the root tsconfig with strict: true.
All exports are re-exported from src/index.ts.
AgentRuntime stub
src/session.ts defines a local AgentRuntime interface with createSession, sendMessage, streamEvents, and getStatus methods when the official SDK does not export a stable TypeScript type.
A code comment marks the stub as pre-GA and names the import path to switch to at GA.
Calls runtime.createSession(config) and wraps the returned session ID in a ManagedAgentsSession instance.
Returns Promise<SessionBase>.
ManagedAgentsSession — execute(toolName, params)
Throws ConcurrentOperationError immediately if _activeMutex is set.
Throws InvalidToolNameError if toolName does not match /^[a-zA-Z0-9_-]+$/.
Calls policyHook.evaluate(agentId, toolName) before sanitizeParams when both are provided. Throws PolicyViolationError if decision.allowed is false. Throws ApprovalRequiredError if decision.requiresApproval is true.
Applies sanitizeParams to params after the policy check.
Acquires the mutex, calls runtime.sendMessage, drains via _drainForToolResult, and resets _activeMutex = null in a finally block.
_drainForToolResult skips events where typeof raw.type !== "string". Throws DrainTimeoutError when Date.now() exceeds the deadline. Returns a ToolResult on raw.type === "tool_result". Throws on raw.type === "done" without a result.
JSDoc on execute states that callers must not automatically retry after DrainTimeoutError because the remote operation may have already executed.
ManagedAgentsSession — send(message)
Throws ConcurrentOperationError if _activeMutex is set.
Acquires the mutex, calls runtime.sendMessage, drains via _drainForSendResult, and resets _activeMutex = null in a finally block.
Returns Promise<SendResult>.
ManagedAgentsSession — sendStream(message)
Throws ConcurrentOperationError if _activeMutex is set.
Acquires the mutex, calls runtime.sendMessage, yields mapped StreamEvent values, breaks on raw.type === "done", and resets _activeMutex = null in a finally block.
Skips events where typeof raw.type !== "string".
Returns AsyncIterable<StreamEvent>.
ManagedAgentsSession — connections() and close()
connections() returns [{ id: sessionId, connected: status === "active" }].
close() sets _status = "closed" and awaits the active mutex (if any) before returning.
ConcurrentOperationError — message: "session already has an operation in progress".
DrainTimeoutError — carries timeoutMs: number.
All five extend Error with correct name properties set in constructors.
Implements SessionBase
TypeScript compiles without error when ManagedAgentsSession is assigned to a variable typed as SessionBase.
Validation
Run from the repository root after implementing:
#!/usr/bin/env bashset -euo pipefail
ADAPTER="packages/managed-agents-adapter"echo"==> Build check"
npx tsc --project "$ADAPTER/tsconfig.json" --noEmit
echo"==> SessionBase assignability check"# Create a temporary type-check file and delete it after
TMPFILE=$(mktemp /tmp/session-check-XXXXXX.ts)
cat >"$TMPFILE"<<'TS'import type { SessionBase } from "@codespar/session-contract";import { createManagedAgentsSession } from "@codespar/managed-agents-adapter";// If ManagedAgentsSession does not implement SessionBase, this assignment fails at compile time.const _check: () => Promise<SessionBase> = () =>createManagedAgentsSession({} as any,{ agentId: "a", environmentId: "e" });TS
npx tsc --strict --noEmit --esModuleInterop "$TMPFILE"|| (rm "$TMPFILE";exit 1)
rm "$TMPFILE"echo" SessionBase assignability: PASS"echo"==> Unit: InvalidToolNameError thrown for newline in tool name"
node --input-type=module <<'JS'import { createManagedAgentsSession } from "./packages/managed-agents-adapter/src/index.js";import { InvalidToolNameError } from "./packages/managed-agents-adapter/src/errors.js";const mockRuntime = {createSession: async () => "session-1",sendMessage: async () => {},streamEvents: async function*() {},getStatus: async () => ({ state: "active" }),};const session = await createManagedAgentsSession(mockRuntime, { agentId: "a", environmentId: "e" });try {await session.execute("tool\nname", {});console.error("FAIL: expected InvalidToolNameError");process.exit(1);} catch (err) {if (err instanceof InvalidToolNameError) {console.log(" InvalidToolNameError for newline: PASS");} else {console.error("FAIL: wrong error type", err);process.exit(1);}}JSecho"==> Unit: ConcurrentOperationError thrown for overlapping calls"
node --input-type=module <<'JS'import { createManagedAgentsSession } from "./packages/managed-agents-adapter/src/index.js";import { ConcurrentOperationError } from "./packages/managed-agents-adapter/src/errors.js";let resolveStream;const mockRuntime = {createSession: async () => "session-1",sendMessage: async () => {},streamEvents: async function*() {// Hang until test resolvesawait new Promise(r => { resolveStream = r; });yield { type: "done" };},getStatus: async () => ({ state: "active" }),};const session = await createManagedAgentsSession(mockRuntime, { agentId: "a", environmentId: "e" });// Start a send that will block in streamEventsconst first = session.send("hello");try {await session.send("concurrent");console.error("FAIL: expected ConcurrentOperationError");resolveStream?.();process.exit(1);} catch (err) {if (err instanceof ConcurrentOperationError) {console.log(" ConcurrentOperationError for concurrent calls: PASS");} else {console.error("FAIL: wrong error type", err);resolveStream?.();process.exit(1);}}resolveStream?.();await first.catch(() => {});JSecho"==> Unit: mutex resets after rejection"
node --input-type=module <<'JS'import { createManagedAgentsSession } from "./packages/managed-agents-adapter/src/index.js";const mockRuntime = {createSession: async () => "session-1",sendMessage: async () => { throw new Error("network error"); },streamEvents: async function*() {},getStatus: async () => ({ state: "active" }),};const session = await createManagedAgentsSession(mockRuntime, { agentId: "a", environmentId: "e" });// First call should rejectawait session.send("fail").catch(() => {});// Second call must not throw ConcurrentOperationError — mutex must have been resettry {await session.send("retry").catch(() => {}); // may reject again (network), but not with concurrent errorconsole.log(" Mutex resets after rejection: PASS");} catch (err) {if (err?.constructor?.name === "ConcurrentOperationError") {console.error("FAIL: mutex was not reset after first rejection");process.exit(1);}console.log(" Mutex resets after rejection: PASS (other error expected)");}JSecho""echo"All validation checks passed."
Dependencies
Issue feat(session-contract): scaffold @codespar/session-contract package #2 (feat(session-contract): publish @codespar/session-contract package) — the SessionBase interface, ToolResult, SendResult, StreamEvent, and BaseConnection types must be published before this adapter can implement them.
Downstream Dependencies
None. This is a leaf node in the milestone dependency graph.
Security Checklist
Tool name injection guard — execute() rejects any toolName that does not match /^[a-zA-Z0-9_-]+$/ before constructing or sending any message. The check runs before policy evaluation. Whitespace or newline characters in a tool name would be embedded verbatim in the JSON payload sent to the Managed Agents API.
PolicyHook evaluation order — policyHook.evaluate(agentId, toolName) runs on the original params before sanitizeParams is called. Sanitizing first could strip fields (such as amount) that the policy hook uses to enforce fund-transfer caps. The ordering is documented in a code comment at the call site.
PolicyHook call signature — uses positional args evaluate(agentId, toolName), not an object argument. Reads decision.allowed (boolean) and decision.requiresApproval (boolean | undefined). Does not reference decision.outcome or any other field.
Mutex try/finally in all three methods — execute, send, and sendStream each set _activeMutex = null inside a finally block. This is verified: a single rejected operation must not permanently block the mutex.
AgentEvent type validation before consumption — _drainForToolResult and sendStream skip events where typeof raw.type !== "string". A malformed event type would produce undefined in the mapped result, making a failed tool call appear successful to the caller.
DrainTimeoutError no-retry documentation — JSDoc on execute() states explicitly that callers must not automatically retry commerce tools (Pix transfers, NF-e issuance) after DrainTimeoutError. The remote operation may have already executed; retrying risks a duplicate transaction.
PII in params — params passed to execute() may contain PII (CPF, CNPJ, Pix amounts, account details). The sanitizeParams option provides adapter-level masking as a last-resort fallback; the commerce-governance layer must apply data classification upstream of this adapter before params reach it. This is documented in JSDoc on createManagedAgentsSession.
Goal
Add
packages/managed-agents-adapter/to codespar-core. This package bridges the Anthropic Managed Agents SDK to theSessionBaseinterface defined in@codespar/session-contract(issue #2). Callers typed againstSessionBasework unchanged regardless of which runtime backs the session.This is a pre-GA implementation. The Anthropic Managed Agents API is in beta; the adapter stubs the
AgentRuntimetype locally and will switch to the official SDK export at GA without changing the public surface.Context
SessionBase(delivered in issue #2) is a runtime-agnostic interface for agent sessions:execute,send,sendStream,connections,close. This adapter provides the Managed Agents-backed implementation of that interface.The adapter is an internal package — it does not bump
@codespar/session-contractwhen updated. Its only public export iscreateManagedAgentsSessionplus the five error classes.Commerce agents call tools with financial consequence (Pix transfers, NF-e issuance). The adapter enforces two non-optional safety properties before any tool execution reaches the Managed Agents API:
/^[a-zA-Z0-9_-]+$/. Whitespace or newline characters in a tool name would be serialized into the synthesized JSON message and could inject instructions into the runtime.PolicyHookruns on the original params beforesanitizeParamstransforms them. Reversing the order would let a caller strip anamountfield and bypass the fund-transfer cap check.A mutex guards each state-mutating method (
execute,send,sendStream). The mutex is reset in afinallyblock in all three paths. Without thefinally, a single rejected operation permanently blocks the mutex and every subsequent call hangs.DrainTimeoutErrormeans the session state is indeterminate. The adapter documents in JSDoc that callers must not automatically retry commerce tools after this error — the tool call may or may not have executed on the remote side.Acceptance Criteria
Package scaffold
packages/managed-agents-adapter/package.jsonsetsname: "@codespar/managed-agents-adapter",version: "0.1.0", and lists@codespar/session-contractas a peer dependency.packages/managed-agents-adapter/tsconfig.jsonextends the root tsconfig withstrict: true.src/index.ts.AgentRuntime stub
src/session.tsdefines a localAgentRuntimeinterface withcreateSession,sendMessage,streamEvents, andgetStatusmethods when the official SDK does not export a stable TypeScript type.createManagedAgentsSessionruntime: AgentRuntime,config: { agentId: string; environmentId: string }, and optionaloptions: { policyHook?, sanitizeParams?, drainTimeoutMs? }.runtime.createSession(config)and wraps the returned session ID in aManagedAgentsSessioninstance.Promise<SessionBase>.ManagedAgentsSession—execute(toolName, params)ConcurrentOperationErrorimmediately if_activeMutexis set.InvalidToolNameErroriftoolNamedoes not match/^[a-zA-Z0-9_-]+$/.policyHook.evaluate(agentId, toolName)beforesanitizeParamswhen both are provided. ThrowsPolicyViolationErrorifdecision.allowedisfalse. ThrowsApprovalRequiredErrorifdecision.requiresApprovalistrue.sanitizeParamsto params after the policy check.runtime.sendMessage, drains via_drainForToolResult, and resets_activeMutex = nullin afinallyblock._drainForToolResultskips events wheretypeof raw.type !== "string". ThrowsDrainTimeoutErrorwhenDate.now()exceeds the deadline. Returns aToolResultonraw.type === "tool_result". Throws onraw.type === "done"without a result.executestates that callers must not automatically retry afterDrainTimeoutErrorbecause the remote operation may have already executed.ManagedAgentsSession—send(message)ConcurrentOperationErrorif_activeMutexis set.runtime.sendMessage, drains via_drainForSendResult, and resets_activeMutex = nullin afinallyblock.Promise<SendResult>.ManagedAgentsSession—sendStream(message)ConcurrentOperationErrorif_activeMutexis set.runtime.sendMessage, yields mappedStreamEventvalues, breaks onraw.type === "done", and resets_activeMutex = nullin afinallyblock.typeof raw.type !== "string".AsyncIterable<StreamEvent>.ManagedAgentsSession—connections()andclose()connections()returns[{ id: sessionId, connected: status === "active" }].close()sets_status = "closed"and awaits the active mutex (if any) before returning.Error classes (
src/errors.ts)InvalidToolNameError— carriestoolName: string.PolicyViolationError— carriesdecision: PolicyDecision.ApprovalRequiredError— carriesdecision: PolicyDecision.ConcurrentOperationError— message:"session already has an operation in progress".DrainTimeoutError— carriestimeoutMs: number.Errorwith correctnameproperties set in constructors.Implements
SessionBaseManagedAgentsSessionis assigned to a variable typed asSessionBase.Validation
Run from the repository root after implementing:
Dependencies
feat(session-contract): publish @codespar/session-contract package) — theSessionBaseinterface,ToolResult,SendResult,StreamEvent, andBaseConnectiontypes must be published before this adapter can implement them.Downstream Dependencies
None. This is a leaf node in the milestone dependency graph.
Security Checklist
execute()rejects anytoolNamethat does not match/^[a-zA-Z0-9_-]+$/before constructing or sending any message. The check runs before policy evaluation. Whitespace or newline characters in a tool name would be embedded verbatim in the JSON payload sent to the Managed Agents API.policyHook.evaluate(agentId, toolName)runs on the originalparamsbeforesanitizeParamsis called. Sanitizing first could strip fields (such asamount) that the policy hook uses to enforce fund-transfer caps. The ordering is documented in a code comment at the call site.evaluate(agentId, toolName), not an object argument. Readsdecision.allowed(boolean) anddecision.requiresApproval(boolean | undefined). Does not referencedecision.outcomeor any other field.execute,send, andsendStreameach set_activeMutex = nullinside afinallyblock. This is verified: a single rejected operation must not permanently block the mutex._drainForToolResultandsendStreamskip events wheretypeof raw.type !== "string". A malformed event type would produceundefinedin the mapped result, making a failed tool call appear successful to the caller.execute()states explicitly that callers must not automatically retry commerce tools (Pix transfers, NF-e issuance) afterDrainTimeoutError. The remote operation may have already executed; retrying risks a duplicate transaction.execute()may contain PII (CPF, CNPJ, Pix amounts, account details). ThesanitizeParamsoption provides adapter-level masking as a last-resort fallback; the commerce-governance layer must apply data classification upstream of this adapter before params reach it. This is documented in JSDoc oncreateManagedAgentsSession.