Skip to content

Commit fffa1f1

Browse files
committed
feat(core): add cloud agent runtime profiles
Introduce runtime profile configuration, protocol-aware adapter matching, and cloudAgent handoff metadata on task records and handles. Add the prepareTask adapter hook so providers can return session details before execution starts, and cover the new routing and validation paths with tests.
1 parent e99b6c0 commit fffa1f1

7 files changed

Lines changed: 186 additions & 15 deletions

File tree

README.md

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,90 @@
11
# @agent-dispatch/core
22

3-
Provider-neutral runtime contracts and dispatch orchestration for AgentDispatch.
3+
[![npm](https://img.shields.io/npm/v/@agent-dispatch/core.svg)](https://www.npmjs.com/package/@agent-dispatch/core)
4+
[![license](https://img.shields.io/npm/l/@agent-dispatch/core.svg)](https://www.npmjs.com/package/@agent-dispatch/core)
45

5-
This package is the compatibility anchor for all AgentDispatch repositories. Cloud adapters, stores, SDKs, CLIs, and MCP servers depend on these interfaces; `@agent-dispatch/core` does not depend on any adapter package.
6+
Provider-neutral runtime primitives for AgentDispatch: the MCP control plane that lets a lead agent spawn, monitor, and interact with cloud subagents without binding itself to one cloud API.
67

7-
## Core concepts
8+
## Why this package exists
89

9-
- Providers: `aws`, `gcp`, `azure`, `kubernetes`, `local`, or future provider strings.
10-
- Capabilities: `agent-runtime` in V1, with reserved support for `service-deploy`, `job-runner`, `container-task`, and `workflow-runner`.
11-
- Tasks: durable work units with provider-neutral lifecycle state.
12-
- Adapters: provider-specific implementations behind a stable `BackendAdapter` contract.
13-
- Policies: provider-neutral authorization rules for account profiles, capabilities, task types, and target modes.
14-
- Testing: `assertBackendAdapterContract` gives adapter repos a reusable conformance check.
15-
16-
## Publishing
10+
Agents should not need to know whether a long-running task runs on AWS AgentCore, Cloud Run, Azure Container Apps, Kubernetes, or a local worker. They should ask for a capability, account profile, task type, and target. `@agent-dispatch/core` defines that stable contract and keeps every adapter compatible with the rest of the system.
1711

1812
See the [release workflow](https://github.com/agent-dispatch/core/blob/main/docs/release.md) for the npm publishing workflow and the dependency-order checklist for the separate `agent-dispatch` repositories.
13+
14+
```mermaid
15+
flowchart LR
16+
A["Lead agent\n(OpenClaw, Hermes, Codex, Claude Code)"] --> B["AgentDispatch core"]
17+
B --> C["Backend adapter"]
18+
C --> D["Cloud runtime"]
19+
D --> E["Cloud subagent"]
20+
```
21+
22+
## What core owns
23+
24+
- Provider-neutral models for `Provider`, `Capability`, `AccountProfile`, `Target`, `Task`, `Runtime`, `Session`, `Event`, and `Artifact`.
25+
- Adapter registration and routing by `provider + capability + task_type + target.mode`.
26+
- Durable task lifecycle orchestration with pluggable stores.
27+
- Runtime protocol metadata for A2A, MCP, AG-UI, and HTTP interaction planes.
28+
- Clarification-safe request validation so MCP servers can ask for missing runtime inputs before dispatch.
29+
30+
## Adapter contract
31+
32+
Every provider adapter implements the same shape:
33+
34+
```ts
35+
interface BackendAdapter {
36+
provider: string;
37+
capabilities(): AdapterCapability[];
38+
resolveTarget(request: DispatchRequest): Promise<ResolvedTarget>;
39+
provision(request: ProvisionRequest): Promise<ProvisionResult>;
40+
prepareTask?(request: PrepareTaskRequest): Promise<PrepareTaskResult>;
41+
startTask(request: StartTaskRequest): Promise<StartTaskResult>;
42+
streamEvents(taskId: string): AsyncIterable<RuntimeEvent>;
43+
cancel(taskId: string): Promise<CancelResult>;
44+
cleanup(target: RuntimeTarget): Promise<CleanupResult>;
45+
}
46+
```
47+
48+
Adapters translate provider-neutral requests into provider APIs, then return provider-neutral task handles, events, artifacts, and optional `cloudAgent` metadata. Provider-specific values stay in adapter config or `target.details`; the MCP server and SDK do not import provider SDK types.
49+
50+
## Install
51+
52+
```bash
53+
npm install @agent-dispatch/core
54+
```
55+
56+
## Example
57+
58+
```ts
59+
import { RuntimeService } from "@agent-dispatch/core";
60+
61+
const runtime = new RuntimeService({
62+
adapters: [awsAgentCoreAdapter],
63+
store,
64+
config
65+
});
66+
67+
const task = await runtime.dispatchTask({
68+
provider: "aws",
69+
accountProfile: "dev-aws",
70+
capability: "agent-runtime",
71+
taskType: "agent.run",
72+
target: { mode: "session", protocol: "a2a" },
73+
input: {
74+
instruction: "Analyze this repository and produce a migration plan."
75+
}
76+
});
77+
```
78+
79+
## Package role
80+
81+
`@agent-dispatch/core` is the compatibility anchor. All adapters depend on it. It depends on no adapters. If a future provider needs a new feature, the design goal is to extend core once and keep the agent-facing MCP tools stable.
82+
83+
## Development
84+
85+
```bash
86+
npm install
87+
npm run typecheck
88+
npm test
89+
npm run build
90+
```

src/adapter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type {
33
CancelResult,
44
CleanupResult,
55
DispatchRequest,
6+
PrepareTaskRequest,
7+
PrepareTaskResult,
68
ProvisionRequest,
79
ProvisionResult,
810
ResolvedTarget,
@@ -17,6 +19,7 @@ export interface BackendAdapter {
1719
readonly provider: string;
1820

1921
capabilities(): AdapterCapability[];
22+
prepareTask?(request: PrepareTaskRequest): Promise<PrepareTaskResult>;
2023
resolveTarget(request: DispatchRequest): Promise<ResolvedTarget>;
2124
provision(request: ProvisionRequest): Promise<ProvisionResult>;
2225
startTask(request: StartTaskRequest): Promise<StartTaskResult>;
@@ -31,7 +34,8 @@ export function adapterSupports(adapter: BackendAdapter, request: DispatchReques
3134
capability.provider === request.provider &&
3235
capability.capability === request.capability &&
3336
capability.taskTypes.includes(request.taskType) &&
34-
capability.targetModes.includes(request.target.mode)
37+
capability.targetModes.includes(request.target.mode) &&
38+
(!request.target.protocol || !capability.protocols || capability.protocols.includes(request.target.protocol))
3539
);
3640
});
3741
}

src/config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { AccountProfile, Capability, DispatchPolicy, DispatchTarget, Provider, TargetMode } from "./types.js";
1+
import type { AccountProfile, Capability, DispatchPolicy, DispatchTarget, Provider, RuntimeProtocol, TargetMode } from "./types.js";
2+
3+
export type RuntimeModelConfig = string | Record<string, unknown>;
24

35
export interface BackendConfig {
46
provider: Provider;
@@ -14,8 +16,11 @@ export interface RuntimeProfileConfig {
1416
capability: Capability;
1517
backend: string;
1618
target?: Partial<DispatchTarget> & { mode: TargetMode };
19+
protocol?: RuntimeProtocol;
1720
framework?: string;
21+
model?: RuntimeModelConfig;
1822
runtimeTools?: Record<string, unknown>;
23+
requiredInputs?: string[];
1924
metadata?: Record<string, unknown>;
2025
}
2126

@@ -33,7 +38,9 @@ export interface AgentDispatchConfig {
3338
capability?: Capability;
3439
backend?: string;
3540
targetMode?: TargetMode;
41+
protocol?: RuntimeProtocol;
3642
framework?: string;
43+
model?: RuntimeModelConfig;
3744
runtimeTools?: Record<string, unknown>;
3845
};
3946
policy?: DispatchPolicy;

src/runtime.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export class RuntimeService {
5656
return getRuntimeProfile(this.config, name);
5757
}
5858

59+
getBackend(name: string) {
60+
const backend = this.config.backends[name];
61+
return backend ? { name, ...backend } : undefined;
62+
}
63+
5964
getDefaultRuntimeProfile() {
6065
return getDefaultRuntimeProfile(this.config);
6166
}
@@ -90,6 +95,13 @@ export class RuntimeService {
9095
const adapter = this.selectAdapter(request);
9196

9297
const task = this.createTaskRecord(request, adapter.name);
98+
const prepared = adapter.prepareTask ? await adapter.prepareTask({ dispatch: request, task }) : undefined;
99+
if (prepared?.providerRefs) {
100+
task.providerRefs = { ...task.providerRefs, ...prepared.providerRefs };
101+
}
102+
if (prepared?.cloudAgent) {
103+
task.cloudAgent = prepared.cloudAgent;
104+
}
93105
await this.store.saveTask(task);
94106
await this.store.appendEvent(this.event(task.id, "task.created", "Task accepted by AgentDispatch."));
95107

@@ -102,6 +114,7 @@ export class RuntimeService {
102114
accountProfile: task.accountProfile,
103115
capability: task.capability,
104116
backend: adapter.name,
117+
cloudAgent: prepared?.cloudAgent,
105118
poll: {
106119
statusTool: "get_task_status",
107120
logsTool: "get_task_logs",
@@ -180,6 +193,7 @@ export class RuntimeService {
180193
await this.store.updateTask(task.id, {
181194
status: "starting",
182195
providerRefs: { ...task.providerRefs, ...provisioned.providerRefs, ...target.providerRefs },
196+
cloudAgent: provisioned.cloudAgent ?? task.cloudAgent,
183197
updatedAt: nowIso()
184198
});
185199
await this.store.appendEvent(this.event(task.id, "task.started", "Starting provider task."));
@@ -200,6 +214,7 @@ export class RuntimeService {
200214
await this.store.updateTask(task.id, {
201215
status: "running",
202216
providerRefs: { ...(await this.latestProviderRefs(task.id)), ...started.providerRefs },
217+
cloudAgent: started.cloudAgent ?? provisioned.cloudAgent ?? task.cloudAgent,
203218
updatedAt: nowIso()
204219
});
205220
for (const artifact of started.artifacts ?? []) {

src/types.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type TaskType = KnownTaskType | (string & {});
2020

2121
export type TargetMode = "session" | "runtime" | "managed-service" | "job" | (string & {});
2222

23+
export type RuntimeProtocol = "http" | "a2a" | "mcp" | "ag-ui" | (string & {});
24+
2325
export type TaskStatus =
2426
| "queued"
2527
| "provisioning"
@@ -62,6 +64,7 @@ export interface AccountProfile {
6264

6365
export interface DispatchTarget {
6466
mode: TargetMode;
67+
protocol?: RuntimeProtocol;
6568
details?: Record<string, unknown>;
6669
}
6770

@@ -111,6 +114,7 @@ export interface AdapterCapability {
111114
capability: Capability;
112115
taskTypes: TaskType[];
113116
targetModes: TargetMode[];
117+
protocols?: RuntimeProtocol[];
114118
configRequirements?: string[];
115119
}
116120

@@ -120,6 +124,7 @@ export interface RuntimeTarget {
120124
capability: Capability;
121125
backend: string;
122126
mode: TargetMode;
127+
protocol?: RuntimeProtocol;
123128
details?: Record<string, unknown>;
124129
providerRefs?: Record<string, unknown>;
125130
}
@@ -167,6 +172,7 @@ export interface TaskRecord {
167172
backend?: string;
168173
status: TaskStatus;
169174
providerRefs: Record<string, unknown>;
175+
cloudAgent?: CloudAgentInteraction;
170176
result?: Record<string, unknown>;
171177
error?: RuntimeErrorShape;
172178
createdAt: string;
@@ -215,10 +221,52 @@ export interface ProvisionRequest {
215221
target: RuntimeTarget;
216222
}
217223

224+
export interface PrepareTaskRequest {
225+
dispatch: DispatchRequest;
226+
task: TaskRecord;
227+
}
228+
229+
export interface ProviderInvocationDetails {
230+
type: string;
231+
provider: Provider;
232+
region?: string;
233+
accountProfile: string;
234+
credentialSource?: string;
235+
[key: string]: unknown;
236+
}
237+
238+
export interface A2AInteractionDetails {
239+
transport: "json-rpc-2.0-http" | (string & {});
240+
messageMethod: "message/send" | (string & {});
241+
agentCardPath?: string;
242+
agentCardOperation?: string;
243+
payloadFormat?: string;
244+
[key: string]: unknown;
245+
}
246+
247+
export interface CloudAgentInteraction {
248+
protocol: RuntimeProtocol;
249+
provider: Provider;
250+
backend: string;
251+
accountProfile: string;
252+
sessionId?: string;
253+
providerRefs?: Record<string, unknown>;
254+
invocation?: ProviderInvocationDetails;
255+
a2a?: A2AInteractionDetails;
256+
model?: unknown;
257+
tools?: Record<string, unknown>;
258+
}
259+
260+
export interface PrepareTaskResult {
261+
providerRefs?: Record<string, unknown>;
262+
cloudAgent?: CloudAgentInteraction;
263+
}
264+
218265
export interface ProvisionResult {
219266
runtime?: RuntimeRecord;
220267
session?: SessionRecord;
221268
providerRefs?: Record<string, unknown>;
269+
cloudAgent?: CloudAgentInteraction;
222270
}
223271

224272
export interface StartTaskRequest {
@@ -231,6 +279,7 @@ export interface StartTaskRequest {
231279

232280
export interface StartTaskResult {
233281
providerRefs?: Record<string, unknown>;
282+
cloudAgent?: CloudAgentInteraction;
234283
result?: Record<string, unknown>;
235284
artifacts?: ArtifactRecord[];
236285
}
@@ -254,6 +303,7 @@ export interface TaskHandle {
254303
accountProfile: string;
255304
capability: Capability;
256305
backend: string;
306+
cloudAgent?: CloudAgentInteraction;
257307
poll: {
258308
statusTool: "get_task_status";
259309
logsTool: "get_task_logs";

test/config.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ const config: AgentDispatchConfig = {
1919
account: "dev-aws",
2020
capability: "agent-runtime",
2121
backend: "aws-agentcore",
22+
protocol: "a2a",
2223
target: { mode: "session", details: { runtimeArn: "arn:aws:bedrock-agentcore:test" } },
2324
framework: "strands",
25+
model: { provider: "bedrock", modelId: "anthropic.claude-3-5-sonnet" },
2426
runtimeTools: { enabled: ["web-search"] }
2527
}
2628
},
@@ -37,7 +39,8 @@ describe("AgentDispatchConfig runtime profiles", () => {
3739
provider: "aws",
3840
account: "dev-aws",
3941
capability: "agent-runtime",
40-
backend: "aws-agentcore"
42+
backend: "aws-agentcore",
43+
protocol: "a2a"
4144
});
4245
expect(getDefaultRuntimeProfile(config)).toMatchObject({ name: "research-agent" });
4346
});

test/runtime.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ function mockAdapter(events: RuntimeEvent[], cleanup: CleanupResult = { status:
4949
name: "mock-agent-runtime",
5050
provider: "aws",
5151
capabilities: () => [{ provider: "aws", capability: "agent-runtime", taskTypes: ["agent.run"], targetModes: ["session"] }],
52+
prepareTask: async ({ dispatch }) => ({
53+
providerRefs: { runtimeSessionId: "agentcore_session_mock" },
54+
cloudAgent: {
55+
protocol: dispatch.target.protocol ?? "a2a",
56+
provider: "aws",
57+
backend: "mock-agent-runtime",
58+
accountProfile: dispatch.accountProfile,
59+
sessionId: "agentcore_session_mock",
60+
providerRefs: { runtimeSessionId: "agentcore_session_mock" },
61+
a2a: {
62+
transport: "json-rpc-2.0-http",
63+
messageMethod: "message/send"
64+
}
65+
}
66+
}),
5267
resolveTarget: async (request) => ({
5368
account: { name: request.accountProfile, provider: "aws", credentialSource: "test" },
5469
target: { provider: "aws", accountProfile: request.accountProfile, capability: "agent-runtime", backend: "mock-agent-runtime", mode: "session" }
@@ -73,7 +88,7 @@ describe("RuntimeService", () => {
7388
accountProfile: "dev-aws",
7489
capability: "agent-runtime",
7590
taskType: "agent.run",
76-
target: { mode: "session" },
91+
target: { mode: "session", protocol: "a2a" },
7792
input: { instruction: "run" }
7893
};
7994
const service = new RuntimeService({
@@ -88,10 +103,15 @@ describe("RuntimeService", () => {
88103
const handle = await service.dispatchTask(request);
89104
expect(handle.provider).toBe("aws");
90105
expect(handle.capability).toBe("agent-runtime");
106+
expect(handle.cloudAgent).toMatchObject({
107+
protocol: "a2a",
108+
sessionId: "agentcore_session_mock"
109+
});
91110

92111
await new Promise((resolve) => setTimeout(resolve, 10));
93112
const task = await service.getTaskStatus(handle.taskId);
94113
expect(["running", "succeeded"]).toContain(task.status);
114+
expect(task.cloudAgent).toMatchObject({ protocol: "a2a" });
95115
});
96116

97117
it("applies configured dispatch policy before adapter selection", async () => {

0 commit comments

Comments
 (0)