@gopherhole/sdk 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/index.d.mts +127 -5
- package/dist/index.d.ts +127 -5
- package/dist/index.js +212 -3
- package/dist/index.mjs +209 -2
- package/package.json +1 -1
- package/src/agent.ts +305 -0
- package/src/index.ts +74 -3
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ new GopherHole(options: GopherHoleOptions)
|
|
|
48
48
|
- `autoReconnect` - Auto-reconnect on disconnect (default: true)
|
|
49
49
|
- `reconnectDelay` - Initial reconnect delay in ms (default: 1000)
|
|
50
50
|
- `maxReconnectAttempts` - Max reconnect attempts (default: 10)
|
|
51
|
+
- `requestTimeout` - Default HTTP request timeout in ms (default: 30000)
|
|
51
52
|
|
|
52
53
|
### Methods
|
|
53
54
|
|
|
@@ -63,6 +64,25 @@ Send a message to another agent.
|
|
|
63
64
|
#### `sendText(toAgentId: string, text: string, options?: SendOptions): Promise<Task>`
|
|
64
65
|
Send a text message to another agent.
|
|
65
66
|
|
|
67
|
+
#### `sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task>`
|
|
68
|
+
Send a text message and wait for the task to complete. Polls until the task reaches a terminal state.
|
|
69
|
+
|
|
70
|
+
**SendAndWaitOptions:**
|
|
71
|
+
- `timeoutMs` - Request timeout in ms (overrides default)
|
|
72
|
+
- `pollIntervalMs` - Polling interval in ms (default: 1000)
|
|
73
|
+
- `maxWaitMs` - Maximum wait time in ms (default: 300000 = 5 min)
|
|
74
|
+
|
|
75
|
+
#### `askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string>`
|
|
76
|
+
Send a text message and wait for the text response. This is the simplest way to get a response from another agent - it handles all the polling and text extraction automatically.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const response = await hub.askText('weather-agent', 'What is the weather in Auckland?');
|
|
80
|
+
console.log(response); // "Currently 18°C and sunny in Auckland"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `waitForTask(taskId: string, options?: SendAndWaitOptions): Promise<Task>`
|
|
84
|
+
Wait for an existing task to complete by polling.
|
|
85
|
+
|
|
66
86
|
#### `reply(taskId: string, payload: MessagePayload): Promise<Task>`
|
|
67
87
|
Reply to an existing conversation.
|
|
68
88
|
|
|
@@ -102,6 +122,21 @@ hub.on('error', (error) => {
|
|
|
102
122
|
});
|
|
103
123
|
```
|
|
104
124
|
|
|
125
|
+
### Helper Functions
|
|
126
|
+
|
|
127
|
+
#### `getTaskResponseText(task: Task): string`
|
|
128
|
+
Extract text response from a completed task. Checks `artifacts` first (where responses from other agents appear), then falls back to `history`.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { GopherHole, getTaskResponseText } from '@gopherhole/sdk';
|
|
132
|
+
|
|
133
|
+
const task = await hub.sendTextAndWait('agent-id', 'Hello!');
|
|
134
|
+
const responseText = getTaskResponseText(task);
|
|
135
|
+
console.log(responseText);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> **Note:** Response text is typically found in `task.artifacts[].parts`, not `task.history`. Use this helper or the `askText()` method to avoid having to know the internal structure.
|
|
139
|
+
|
|
105
140
|
### Types
|
|
106
141
|
|
|
107
142
|
```typescript
|
|
@@ -142,6 +177,30 @@ interface TaskStatus {
|
|
|
142
177
|
|
|
143
178
|
## Examples
|
|
144
179
|
|
|
180
|
+
### Send and Wait for Response
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { GopherHole, getTaskResponseText } from '@gopherhole/sdk';
|
|
184
|
+
|
|
185
|
+
const hub = new GopherHole({
|
|
186
|
+
apiKey: process.env.GOPHERHOLE_API_KEY!,
|
|
187
|
+
requestTimeout: 60000, // 60s default timeout
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Option 1: Use askText() for simplest usage
|
|
191
|
+
const response = await hub.askText('weather-agent', 'What is the weather in Auckland?');
|
|
192
|
+
console.log('Response:', response);
|
|
193
|
+
|
|
194
|
+
// Option 2: Use sendTextAndWait() with helper function for more control
|
|
195
|
+
const task = await hub.sendTextAndWait('weather-agent', 'What is the weather in Auckland?', {
|
|
196
|
+
maxWaitMs: 120000, // Wait up to 2 minutes
|
|
197
|
+
pollIntervalMs: 2000, // Poll every 2 seconds
|
|
198
|
+
});
|
|
199
|
+
const responseText = getTaskResponseText(task);
|
|
200
|
+
console.log('Response:', responseText);
|
|
201
|
+
console.log('Task status:', task.status.state);
|
|
202
|
+
```
|
|
203
|
+
|
|
145
204
|
### Echo Bot
|
|
146
205
|
|
|
147
206
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -97,13 +97,13 @@ interface A2AArtifact {
|
|
|
97
97
|
lastChunk?: boolean;
|
|
98
98
|
metadata?: Record<string, unknown>;
|
|
99
99
|
}
|
|
100
|
-
interface JsonRpcRequest {
|
|
100
|
+
interface JsonRpcRequest$1 {
|
|
101
101
|
jsonrpc: '2.0';
|
|
102
102
|
method: string;
|
|
103
103
|
params?: Record<string, unknown>;
|
|
104
104
|
id: string | number;
|
|
105
105
|
}
|
|
106
|
-
interface JsonRpcResponse<T = unknown> {
|
|
106
|
+
interface JsonRpcResponse$1<T = unknown> {
|
|
107
107
|
jsonrpc: '2.0';
|
|
108
108
|
result?: T;
|
|
109
109
|
error?: JsonRpcError;
|
|
@@ -179,6 +179,116 @@ interface TaskArtifactUpdateEvent {
|
|
|
179
179
|
}
|
|
180
180
|
type TaskEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* GopherHoleAgent - Helper for building webhook-based A2A agents
|
|
184
|
+
*
|
|
185
|
+
* Use this to create agents that receive requests from the GopherHole hub
|
|
186
|
+
* via webhooks (e.g., Cloudflare Workers, Express servers).
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
interface IncomingMessage {
|
|
190
|
+
role: 'user' | 'agent';
|
|
191
|
+
parts: AgentMessagePart[];
|
|
192
|
+
metadata?: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
interface AgentMessagePart {
|
|
195
|
+
kind: 'text' | 'file' | 'data';
|
|
196
|
+
text?: string;
|
|
197
|
+
mimeType?: string;
|
|
198
|
+
data?: string;
|
|
199
|
+
uri?: string;
|
|
200
|
+
}
|
|
201
|
+
interface AgentTaskResult {
|
|
202
|
+
id: string;
|
|
203
|
+
contextId: string;
|
|
204
|
+
status: AgentTaskStatus;
|
|
205
|
+
messages: IncomingMessage[];
|
|
206
|
+
artifacts?: AgentArtifact[];
|
|
207
|
+
}
|
|
208
|
+
interface AgentTaskStatus {
|
|
209
|
+
state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
|
|
210
|
+
timestamp: string;
|
|
211
|
+
message?: string;
|
|
212
|
+
}
|
|
213
|
+
interface AgentArtifact {
|
|
214
|
+
name?: string;
|
|
215
|
+
mimeType?: string;
|
|
216
|
+
parts?: AgentMessagePart[];
|
|
217
|
+
}
|
|
218
|
+
interface JsonRpcRequest {
|
|
219
|
+
jsonrpc: '2.0';
|
|
220
|
+
method: string;
|
|
221
|
+
params?: Record<string, unknown>;
|
|
222
|
+
id?: string | number;
|
|
223
|
+
}
|
|
224
|
+
interface JsonRpcResponse {
|
|
225
|
+
jsonrpc: '2.0';
|
|
226
|
+
result?: unknown;
|
|
227
|
+
error?: {
|
|
228
|
+
code: number;
|
|
229
|
+
message: string;
|
|
230
|
+
data?: unknown;
|
|
231
|
+
};
|
|
232
|
+
id?: string | number | null;
|
|
233
|
+
}
|
|
234
|
+
interface MessageContext {
|
|
235
|
+
/** The incoming message */
|
|
236
|
+
message: IncomingMessage;
|
|
237
|
+
/** Extracted text content from message parts */
|
|
238
|
+
text: string;
|
|
239
|
+
/** Task ID if provided */
|
|
240
|
+
taskId?: string;
|
|
241
|
+
/** Context ID if provided */
|
|
242
|
+
contextId?: string;
|
|
243
|
+
/** Full params from JSON-RPC request */
|
|
244
|
+
params: Record<string, unknown>;
|
|
245
|
+
}
|
|
246
|
+
type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
|
|
247
|
+
interface GopherHoleAgentOptions {
|
|
248
|
+
/** Agent card for discovery */
|
|
249
|
+
card: AgentCard;
|
|
250
|
+
/** API key for authentication (from GopherHole hub) */
|
|
251
|
+
apiKey?: string;
|
|
252
|
+
/** Handler for incoming messages */
|
|
253
|
+
onMessage: MessageHandler;
|
|
254
|
+
}
|
|
255
|
+
declare class GopherHoleAgent {
|
|
256
|
+
private card;
|
|
257
|
+
private apiKey?;
|
|
258
|
+
private onMessage;
|
|
259
|
+
constructor(options: GopherHoleAgentOptions);
|
|
260
|
+
/** Get the agent card */
|
|
261
|
+
getCard(): AgentCard;
|
|
262
|
+
/** Verify authorization header */
|
|
263
|
+
verifyAuth(authHeader: string | null): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Handle an incoming HTTP request
|
|
266
|
+
* Returns a Response object (works with Cloudflare Workers, Bun, etc.)
|
|
267
|
+
*/
|
|
268
|
+
handleRequest(request: Request): Promise<Response>;
|
|
269
|
+
/**
|
|
270
|
+
* Handle a JSON-RPC request directly
|
|
271
|
+
*/
|
|
272
|
+
handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
273
|
+
private handleMessageSend;
|
|
274
|
+
/**
|
|
275
|
+
* Create a completed task result from a text response
|
|
276
|
+
*/
|
|
277
|
+
createTaskResult(originalMessage: IncomingMessage, responseText: string, contextId?: string): AgentTaskResult;
|
|
278
|
+
/**
|
|
279
|
+
* Helper to create a text message part
|
|
280
|
+
*/
|
|
281
|
+
static textPart(text: string): AgentMessagePart;
|
|
282
|
+
/**
|
|
283
|
+
* Helper to create a file message part
|
|
284
|
+
*/
|
|
285
|
+
static filePart(uri: string, mimeType: string): AgentMessagePart;
|
|
286
|
+
/**
|
|
287
|
+
* Helper to create a data message part (base64)
|
|
288
|
+
*/
|
|
289
|
+
static dataPart(data: string, mimeType: string): AgentMessagePart;
|
|
290
|
+
}
|
|
291
|
+
|
|
182
292
|
interface GopherHoleOptions {
|
|
183
293
|
/** API key (starts with gph_) */
|
|
184
294
|
apiKey: string;
|
|
@@ -243,11 +353,18 @@ interface TaskStatus {
|
|
|
243
353
|
message?: string;
|
|
244
354
|
}
|
|
245
355
|
interface Artifact {
|
|
246
|
-
name
|
|
247
|
-
|
|
356
|
+
name?: string;
|
|
357
|
+
artifactId?: string;
|
|
358
|
+
mimeType?: string;
|
|
359
|
+
parts?: MessagePart[];
|
|
248
360
|
data?: string;
|
|
249
361
|
uri?: string;
|
|
250
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Extract text response from a completed task.
|
|
365
|
+
* Checks artifacts first (where responses live), then falls back to history.
|
|
366
|
+
*/
|
|
367
|
+
declare function getTaskResponseText(task: Task): string;
|
|
251
368
|
interface SendOptions {
|
|
252
369
|
/** Existing context/conversation ID */
|
|
253
370
|
contextId?: string;
|
|
@@ -311,6 +428,11 @@ declare class GopherHole extends EventEmitter<EventMap> {
|
|
|
311
428
|
* Returns the completed task with response artifacts
|
|
312
429
|
*/
|
|
313
430
|
sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task>;
|
|
431
|
+
/**
|
|
432
|
+
* Send a text message and wait for the text response
|
|
433
|
+
* This is a convenience method that extracts the response text automatically
|
|
434
|
+
*/
|
|
435
|
+
askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string>;
|
|
314
436
|
/**
|
|
315
437
|
* Wait for a task to complete (polling)
|
|
316
438
|
*/
|
|
@@ -522,4 +644,4 @@ interface RatingResult {
|
|
|
522
644
|
ratingCount: number;
|
|
523
645
|
}
|
|
524
646
|
|
|
525
|
-
export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentReview, type AgentSkill, type AgentSkillConfig, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, type GopherHoleOptions, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest, type JsonRpcResponse, type Message, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default };
|
|
647
|
+
export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentArtifact, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentMessagePart, type AgentReview, type AgentSkill, type AgentSkillConfig, type AgentTaskResult, type AgentTaskStatus, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, GopherHoleAgent, type GopherHoleAgentOptions, type GopherHoleOptions, type IncomingMessage, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest$1 as JsonRpcRequest, type JsonRpcResponse$1 as JsonRpcResponse, type Message, type MessageContext, type MessageHandler, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type AgentTaskResult as TaskResult, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
|
package/dist/index.d.ts
CHANGED
|
@@ -97,13 +97,13 @@ interface A2AArtifact {
|
|
|
97
97
|
lastChunk?: boolean;
|
|
98
98
|
metadata?: Record<string, unknown>;
|
|
99
99
|
}
|
|
100
|
-
interface JsonRpcRequest {
|
|
100
|
+
interface JsonRpcRequest$1 {
|
|
101
101
|
jsonrpc: '2.0';
|
|
102
102
|
method: string;
|
|
103
103
|
params?: Record<string, unknown>;
|
|
104
104
|
id: string | number;
|
|
105
105
|
}
|
|
106
|
-
interface JsonRpcResponse<T = unknown> {
|
|
106
|
+
interface JsonRpcResponse$1<T = unknown> {
|
|
107
107
|
jsonrpc: '2.0';
|
|
108
108
|
result?: T;
|
|
109
109
|
error?: JsonRpcError;
|
|
@@ -179,6 +179,116 @@ interface TaskArtifactUpdateEvent {
|
|
|
179
179
|
}
|
|
180
180
|
type TaskEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* GopherHoleAgent - Helper for building webhook-based A2A agents
|
|
184
|
+
*
|
|
185
|
+
* Use this to create agents that receive requests from the GopherHole hub
|
|
186
|
+
* via webhooks (e.g., Cloudflare Workers, Express servers).
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
interface IncomingMessage {
|
|
190
|
+
role: 'user' | 'agent';
|
|
191
|
+
parts: AgentMessagePart[];
|
|
192
|
+
metadata?: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
interface AgentMessagePart {
|
|
195
|
+
kind: 'text' | 'file' | 'data';
|
|
196
|
+
text?: string;
|
|
197
|
+
mimeType?: string;
|
|
198
|
+
data?: string;
|
|
199
|
+
uri?: string;
|
|
200
|
+
}
|
|
201
|
+
interface AgentTaskResult {
|
|
202
|
+
id: string;
|
|
203
|
+
contextId: string;
|
|
204
|
+
status: AgentTaskStatus;
|
|
205
|
+
messages: IncomingMessage[];
|
|
206
|
+
artifacts?: AgentArtifact[];
|
|
207
|
+
}
|
|
208
|
+
interface AgentTaskStatus {
|
|
209
|
+
state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
|
|
210
|
+
timestamp: string;
|
|
211
|
+
message?: string;
|
|
212
|
+
}
|
|
213
|
+
interface AgentArtifact {
|
|
214
|
+
name?: string;
|
|
215
|
+
mimeType?: string;
|
|
216
|
+
parts?: AgentMessagePart[];
|
|
217
|
+
}
|
|
218
|
+
interface JsonRpcRequest {
|
|
219
|
+
jsonrpc: '2.0';
|
|
220
|
+
method: string;
|
|
221
|
+
params?: Record<string, unknown>;
|
|
222
|
+
id?: string | number;
|
|
223
|
+
}
|
|
224
|
+
interface JsonRpcResponse {
|
|
225
|
+
jsonrpc: '2.0';
|
|
226
|
+
result?: unknown;
|
|
227
|
+
error?: {
|
|
228
|
+
code: number;
|
|
229
|
+
message: string;
|
|
230
|
+
data?: unknown;
|
|
231
|
+
};
|
|
232
|
+
id?: string | number | null;
|
|
233
|
+
}
|
|
234
|
+
interface MessageContext {
|
|
235
|
+
/** The incoming message */
|
|
236
|
+
message: IncomingMessage;
|
|
237
|
+
/** Extracted text content from message parts */
|
|
238
|
+
text: string;
|
|
239
|
+
/** Task ID if provided */
|
|
240
|
+
taskId?: string;
|
|
241
|
+
/** Context ID if provided */
|
|
242
|
+
contextId?: string;
|
|
243
|
+
/** Full params from JSON-RPC request */
|
|
244
|
+
params: Record<string, unknown>;
|
|
245
|
+
}
|
|
246
|
+
type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
|
|
247
|
+
interface GopherHoleAgentOptions {
|
|
248
|
+
/** Agent card for discovery */
|
|
249
|
+
card: AgentCard;
|
|
250
|
+
/** API key for authentication (from GopherHole hub) */
|
|
251
|
+
apiKey?: string;
|
|
252
|
+
/** Handler for incoming messages */
|
|
253
|
+
onMessage: MessageHandler;
|
|
254
|
+
}
|
|
255
|
+
declare class GopherHoleAgent {
|
|
256
|
+
private card;
|
|
257
|
+
private apiKey?;
|
|
258
|
+
private onMessage;
|
|
259
|
+
constructor(options: GopherHoleAgentOptions);
|
|
260
|
+
/** Get the agent card */
|
|
261
|
+
getCard(): AgentCard;
|
|
262
|
+
/** Verify authorization header */
|
|
263
|
+
verifyAuth(authHeader: string | null): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Handle an incoming HTTP request
|
|
266
|
+
* Returns a Response object (works with Cloudflare Workers, Bun, etc.)
|
|
267
|
+
*/
|
|
268
|
+
handleRequest(request: Request): Promise<Response>;
|
|
269
|
+
/**
|
|
270
|
+
* Handle a JSON-RPC request directly
|
|
271
|
+
*/
|
|
272
|
+
handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
273
|
+
private handleMessageSend;
|
|
274
|
+
/**
|
|
275
|
+
* Create a completed task result from a text response
|
|
276
|
+
*/
|
|
277
|
+
createTaskResult(originalMessage: IncomingMessage, responseText: string, contextId?: string): AgentTaskResult;
|
|
278
|
+
/**
|
|
279
|
+
* Helper to create a text message part
|
|
280
|
+
*/
|
|
281
|
+
static textPart(text: string): AgentMessagePart;
|
|
282
|
+
/**
|
|
283
|
+
* Helper to create a file message part
|
|
284
|
+
*/
|
|
285
|
+
static filePart(uri: string, mimeType: string): AgentMessagePart;
|
|
286
|
+
/**
|
|
287
|
+
* Helper to create a data message part (base64)
|
|
288
|
+
*/
|
|
289
|
+
static dataPart(data: string, mimeType: string): AgentMessagePart;
|
|
290
|
+
}
|
|
291
|
+
|
|
182
292
|
interface GopherHoleOptions {
|
|
183
293
|
/** API key (starts with gph_) */
|
|
184
294
|
apiKey: string;
|
|
@@ -243,11 +353,18 @@ interface TaskStatus {
|
|
|
243
353
|
message?: string;
|
|
244
354
|
}
|
|
245
355
|
interface Artifact {
|
|
246
|
-
name
|
|
247
|
-
|
|
356
|
+
name?: string;
|
|
357
|
+
artifactId?: string;
|
|
358
|
+
mimeType?: string;
|
|
359
|
+
parts?: MessagePart[];
|
|
248
360
|
data?: string;
|
|
249
361
|
uri?: string;
|
|
250
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Extract text response from a completed task.
|
|
365
|
+
* Checks artifacts first (where responses live), then falls back to history.
|
|
366
|
+
*/
|
|
367
|
+
declare function getTaskResponseText(task: Task): string;
|
|
251
368
|
interface SendOptions {
|
|
252
369
|
/** Existing context/conversation ID */
|
|
253
370
|
contextId?: string;
|
|
@@ -311,6 +428,11 @@ declare class GopherHole extends EventEmitter<EventMap> {
|
|
|
311
428
|
* Returns the completed task with response artifacts
|
|
312
429
|
*/
|
|
313
430
|
sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task>;
|
|
431
|
+
/**
|
|
432
|
+
* Send a text message and wait for the text response
|
|
433
|
+
* This is a convenience method that extracts the response text automatically
|
|
434
|
+
*/
|
|
435
|
+
askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string>;
|
|
314
436
|
/**
|
|
315
437
|
* Wait for a task to complete (polling)
|
|
316
438
|
*/
|
|
@@ -522,4 +644,4 @@ interface RatingResult {
|
|
|
522
644
|
ratingCount: number;
|
|
523
645
|
}
|
|
524
646
|
|
|
525
|
-
export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentReview, type AgentSkill, type AgentSkillConfig, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, type GopherHoleOptions, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest, type JsonRpcResponse, type Message, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default };
|
|
647
|
+
export { type A2AArtifact, type A2AMessage, type A2ATask, type A2ATaskStatus, type AgentArtifact, type AgentAuthentication, type AgentCapabilities, type AgentCard, type AgentCardConfig, type AgentCategory, type AgentInfoResult, type AgentMessagePart, type AgentReview, type AgentSkill, type AgentSkillConfig, type AgentTaskResult, type AgentTaskStatus, type Artifact, type ContentMode, type DataContent, type DataPart, type DiscoverOptions, type DiscoverResult, type FileContent, type FilePart, GopherHole, GopherHoleAgent, type GopherHoleAgentOptions, type GopherHoleOptions, type IncomingMessage, type InputMode, type JsonRpcError, JsonRpcErrorCodes, type JsonRpcRequest$1 as JsonRpcRequest, type JsonRpcResponse$1 as JsonRpcResponse, type Message, type MessageContext, type MessageHandler, type MessagePart, type MessagePayload, type MessageSendConfiguration, type OutputMode, type Part, type PublicAgent, type PushNotificationConfig, type RatingResult, type SendAndWaitOptions, type SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type AgentTaskResult as TaskResult, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default, getTaskResponseText };
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
GopherHole: () => GopherHole,
|
|
24
|
+
GopherHoleAgent: () => GopherHoleAgent,
|
|
24
25
|
JsonRpcErrorCodes: () => JsonRpcErrorCodes,
|
|
25
|
-
default: () => index_default
|
|
26
|
+
default: () => index_default,
|
|
27
|
+
getTaskResponseText: () => getTaskResponseText
|
|
26
28
|
});
|
|
27
29
|
module.exports = __toCommonJS(index_exports);
|
|
28
30
|
var import_eventemitter3 = require("eventemitter3");
|
|
@@ -43,7 +45,201 @@ var JsonRpcErrorCodes = {
|
|
|
43
45
|
InvalidAgentCard: -32006
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
// src/agent.ts
|
|
49
|
+
var GopherHoleAgent = class {
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.card = options.card;
|
|
52
|
+
this.apiKey = options.apiKey;
|
|
53
|
+
this.onMessage = options.onMessage;
|
|
54
|
+
}
|
|
55
|
+
/** Get the agent card */
|
|
56
|
+
getCard() {
|
|
57
|
+
return this.card;
|
|
58
|
+
}
|
|
59
|
+
/** Verify authorization header */
|
|
60
|
+
verifyAuth(authHeader) {
|
|
61
|
+
if (!this.apiKey) return true;
|
|
62
|
+
return authHeader === `Bearer ${this.apiKey}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle an incoming HTTP request
|
|
66
|
+
* Returns a Response object (works with Cloudflare Workers, Bun, etc.)
|
|
67
|
+
*/
|
|
68
|
+
async handleRequest(request) {
|
|
69
|
+
const url = new URL(request.url);
|
|
70
|
+
const corsHeaders = {
|
|
71
|
+
"Access-Control-Allow-Origin": "*",
|
|
72
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
73
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
74
|
+
};
|
|
75
|
+
if (request.method === "OPTIONS") {
|
|
76
|
+
return new Response(null, { headers: corsHeaders });
|
|
77
|
+
}
|
|
78
|
+
if (url.pathname === "/.well-known/agent.json" || url.pathname === "/agent.json") {
|
|
79
|
+
return Response.json(this.card, { headers: corsHeaders });
|
|
80
|
+
}
|
|
81
|
+
if (url.pathname === "/health") {
|
|
82
|
+
return Response.json(
|
|
83
|
+
{ status: "ok", agent: this.card.name, version: this.card.version || "1.0.0" },
|
|
84
|
+
{ headers: corsHeaders }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (request.method === "POST" && (url.pathname === "/" || url.pathname === "/a2a")) {
|
|
88
|
+
if (this.apiKey && !this.verifyAuth(request.headers.get("Authorization"))) {
|
|
89
|
+
return Response.json(
|
|
90
|
+
{ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null },
|
|
91
|
+
{ status: 401, headers: corsHeaders }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const body = await request.json();
|
|
96
|
+
const response = await this.handleJsonRpc(body);
|
|
97
|
+
return Response.json(response, { headers: corsHeaders });
|
|
98
|
+
} catch {
|
|
99
|
+
return Response.json(
|
|
100
|
+
{ jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null },
|
|
101
|
+
{ status: 400, headers: corsHeaders }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return new Response("Not Found", { status: 404, headers: corsHeaders });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Handle a JSON-RPC request directly
|
|
109
|
+
*/
|
|
110
|
+
async handleJsonRpc(req) {
|
|
111
|
+
const { method, params = {}, id } = req;
|
|
112
|
+
switch (method) {
|
|
113
|
+
case "message/send":
|
|
114
|
+
return this.handleMessageSend(params, id);
|
|
115
|
+
case "tasks/get":
|
|
116
|
+
return {
|
|
117
|
+
jsonrpc: "2.0",
|
|
118
|
+
error: { code: -32601, message: "This agent does not support persistent tasks" },
|
|
119
|
+
id
|
|
120
|
+
};
|
|
121
|
+
case "tasks/cancel":
|
|
122
|
+
return {
|
|
123
|
+
jsonrpc: "2.0",
|
|
124
|
+
error: { code: -32601, message: "This agent does not support task cancellation" },
|
|
125
|
+
id
|
|
126
|
+
};
|
|
127
|
+
default:
|
|
128
|
+
return {
|
|
129
|
+
jsonrpc: "2.0",
|
|
130
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
131
|
+
id
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async handleMessageSend(params, id) {
|
|
136
|
+
const message = params.message;
|
|
137
|
+
if (!message?.parts) {
|
|
138
|
+
return {
|
|
139
|
+
jsonrpc: "2.0",
|
|
140
|
+
error: { code: -32602, message: "Invalid params: message with parts required" },
|
|
141
|
+
id
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const text = message.parts.filter((p) => p.kind === "text" || p.text).map((p) => p.text || "").join("\n").trim();
|
|
145
|
+
const config = params.configuration;
|
|
146
|
+
const ctx = {
|
|
147
|
+
message,
|
|
148
|
+
text,
|
|
149
|
+
taskId: params.taskId,
|
|
150
|
+
contextId: config?.contextId,
|
|
151
|
+
params
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const result = await this.onMessage(ctx);
|
|
155
|
+
if (typeof result === "string") {
|
|
156
|
+
const taskResult = this.createTaskResult(message, result, ctx.contextId);
|
|
157
|
+
return { jsonrpc: "2.0", result: taskResult, id };
|
|
158
|
+
}
|
|
159
|
+
return { jsonrpc: "2.0", result, id };
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMessage = error instanceof Error ? error.message : "Handler error";
|
|
162
|
+
return {
|
|
163
|
+
jsonrpc: "2.0",
|
|
164
|
+
error: { code: -32e3, message: errorMessage },
|
|
165
|
+
id
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a completed task result from a text response
|
|
171
|
+
*/
|
|
172
|
+
createTaskResult(originalMessage, responseText, contextId) {
|
|
173
|
+
return {
|
|
174
|
+
id: `task-${Date.now()}`,
|
|
175
|
+
contextId: contextId || `ctx-${Date.now()}`,
|
|
176
|
+
status: {
|
|
177
|
+
state: "completed",
|
|
178
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
179
|
+
},
|
|
180
|
+
messages: [
|
|
181
|
+
originalMessage,
|
|
182
|
+
{
|
|
183
|
+
role: "agent",
|
|
184
|
+
parts: [{ kind: "text", text: responseText }],
|
|
185
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Helper to create a text message part
|
|
192
|
+
*/
|
|
193
|
+
static textPart(text) {
|
|
194
|
+
return { kind: "text", text };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Helper to create a file message part
|
|
198
|
+
*/
|
|
199
|
+
static filePart(uri, mimeType) {
|
|
200
|
+
return { kind: "file", uri, mimeType };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Helper to create a data message part (base64)
|
|
204
|
+
*/
|
|
205
|
+
static dataPart(data, mimeType) {
|
|
206
|
+
return { kind: "data", data, mimeType };
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
46
210
|
// src/index.ts
|
|
211
|
+
function getTaskResponseText(task) {
|
|
212
|
+
if (task.artifacts?.length) {
|
|
213
|
+
const texts = [];
|
|
214
|
+
for (const artifact of task.artifacts) {
|
|
215
|
+
if (artifact.parts) {
|
|
216
|
+
for (const part of artifact.parts) {
|
|
217
|
+
if (part.kind === "text" && part.text) {
|
|
218
|
+
texts.push(part.text);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (texts.length > 0) {
|
|
224
|
+
return texts.join("\n");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (task.history?.length) {
|
|
228
|
+
const lastMessage = task.history[task.history.length - 1];
|
|
229
|
+
if (lastMessage.parts) {
|
|
230
|
+
const texts = [];
|
|
231
|
+
for (const part of lastMessage.parts) {
|
|
232
|
+
if (part.kind === "text" && part.text) {
|
|
233
|
+
texts.push(part.text);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (texts.length > 0) {
|
|
237
|
+
return texts.join("\n");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
47
243
|
var DEFAULT_HUB_URL = "wss://gopherhole.helixdata.workers.dev/ws";
|
|
48
244
|
var GopherHole = class extends import_eventemitter3.EventEmitter {
|
|
49
245
|
constructor(apiKeyOrOptions) {
|
|
@@ -160,6 +356,17 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
|
|
|
160
356
|
const task = await this.sendText(toAgentId, text, options);
|
|
161
357
|
return this.waitForTask(task.id, options);
|
|
162
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Send a text message and wait for the text response
|
|
361
|
+
* This is a convenience method that extracts the response text automatically
|
|
362
|
+
*/
|
|
363
|
+
async askText(toAgentId, text, options) {
|
|
364
|
+
const task = await this.sendTextAndWait(toAgentId, text, options);
|
|
365
|
+
if (task.status.state === "failed") {
|
|
366
|
+
throw new Error(task.status.message || "Task failed");
|
|
367
|
+
}
|
|
368
|
+
return getTaskResponseText(task);
|
|
369
|
+
}
|
|
163
370
|
/**
|
|
164
371
|
* Wait for a task to complete (polling)
|
|
165
372
|
*/
|
|
@@ -300,7 +507,7 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
|
|
|
300
507
|
if (this.ws?.readyState === 1) {
|
|
301
508
|
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
302
509
|
}
|
|
303
|
-
},
|
|
510
|
+
}, 15e3);
|
|
304
511
|
}
|
|
305
512
|
/**
|
|
306
513
|
* Stop ping interval
|
|
@@ -494,5 +701,7 @@ var index_default = GopherHole;
|
|
|
494
701
|
// Annotate the CommonJS export names for ESM import in node:
|
|
495
702
|
0 && (module.exports = {
|
|
496
703
|
GopherHole,
|
|
497
|
-
|
|
704
|
+
GopherHoleAgent,
|
|
705
|
+
JsonRpcErrorCodes,
|
|
706
|
+
getTaskResponseText
|
|
498
707
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -24,7 +24,201 @@ var JsonRpcErrorCodes = {
|
|
|
24
24
|
InvalidAgentCard: -32006
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// src/agent.ts
|
|
28
|
+
var GopherHoleAgent = class {
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.card = options.card;
|
|
31
|
+
this.apiKey = options.apiKey;
|
|
32
|
+
this.onMessage = options.onMessage;
|
|
33
|
+
}
|
|
34
|
+
/** Get the agent card */
|
|
35
|
+
getCard() {
|
|
36
|
+
return this.card;
|
|
37
|
+
}
|
|
38
|
+
/** Verify authorization header */
|
|
39
|
+
verifyAuth(authHeader) {
|
|
40
|
+
if (!this.apiKey) return true;
|
|
41
|
+
return authHeader === `Bearer ${this.apiKey}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Handle an incoming HTTP request
|
|
45
|
+
* Returns a Response object (works with Cloudflare Workers, Bun, etc.)
|
|
46
|
+
*/
|
|
47
|
+
async handleRequest(request) {
|
|
48
|
+
const url = new URL(request.url);
|
|
49
|
+
const corsHeaders = {
|
|
50
|
+
"Access-Control-Allow-Origin": "*",
|
|
51
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
52
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
53
|
+
};
|
|
54
|
+
if (request.method === "OPTIONS") {
|
|
55
|
+
return new Response(null, { headers: corsHeaders });
|
|
56
|
+
}
|
|
57
|
+
if (url.pathname === "/.well-known/agent.json" || url.pathname === "/agent.json") {
|
|
58
|
+
return Response.json(this.card, { headers: corsHeaders });
|
|
59
|
+
}
|
|
60
|
+
if (url.pathname === "/health") {
|
|
61
|
+
return Response.json(
|
|
62
|
+
{ status: "ok", agent: this.card.name, version: this.card.version || "1.0.0" },
|
|
63
|
+
{ headers: corsHeaders }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (request.method === "POST" && (url.pathname === "/" || url.pathname === "/a2a")) {
|
|
67
|
+
if (this.apiKey && !this.verifyAuth(request.headers.get("Authorization"))) {
|
|
68
|
+
return Response.json(
|
|
69
|
+
{ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null },
|
|
70
|
+
{ status: 401, headers: corsHeaders }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const body = await request.json();
|
|
75
|
+
const response = await this.handleJsonRpc(body);
|
|
76
|
+
return Response.json(response, { headers: corsHeaders });
|
|
77
|
+
} catch {
|
|
78
|
+
return Response.json(
|
|
79
|
+
{ jsonrpc: "2.0", error: { code: -32700, message: "Parse error" }, id: null },
|
|
80
|
+
{ status: 400, headers: corsHeaders }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return new Response("Not Found", { status: 404, headers: corsHeaders });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Handle a JSON-RPC request directly
|
|
88
|
+
*/
|
|
89
|
+
async handleJsonRpc(req) {
|
|
90
|
+
const { method, params = {}, id } = req;
|
|
91
|
+
switch (method) {
|
|
92
|
+
case "message/send":
|
|
93
|
+
return this.handleMessageSend(params, id);
|
|
94
|
+
case "tasks/get":
|
|
95
|
+
return {
|
|
96
|
+
jsonrpc: "2.0",
|
|
97
|
+
error: { code: -32601, message: "This agent does not support persistent tasks" },
|
|
98
|
+
id
|
|
99
|
+
};
|
|
100
|
+
case "tasks/cancel":
|
|
101
|
+
return {
|
|
102
|
+
jsonrpc: "2.0",
|
|
103
|
+
error: { code: -32601, message: "This agent does not support task cancellation" },
|
|
104
|
+
id
|
|
105
|
+
};
|
|
106
|
+
default:
|
|
107
|
+
return {
|
|
108
|
+
jsonrpc: "2.0",
|
|
109
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
110
|
+
id
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async handleMessageSend(params, id) {
|
|
115
|
+
const message = params.message;
|
|
116
|
+
if (!message?.parts) {
|
|
117
|
+
return {
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
error: { code: -32602, message: "Invalid params: message with parts required" },
|
|
120
|
+
id
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const text = message.parts.filter((p) => p.kind === "text" || p.text).map((p) => p.text || "").join("\n").trim();
|
|
124
|
+
const config = params.configuration;
|
|
125
|
+
const ctx = {
|
|
126
|
+
message,
|
|
127
|
+
text,
|
|
128
|
+
taskId: params.taskId,
|
|
129
|
+
contextId: config?.contextId,
|
|
130
|
+
params
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
const result = await this.onMessage(ctx);
|
|
134
|
+
if (typeof result === "string") {
|
|
135
|
+
const taskResult = this.createTaskResult(message, result, ctx.contextId);
|
|
136
|
+
return { jsonrpc: "2.0", result: taskResult, id };
|
|
137
|
+
}
|
|
138
|
+
return { jsonrpc: "2.0", result, id };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : "Handler error";
|
|
141
|
+
return {
|
|
142
|
+
jsonrpc: "2.0",
|
|
143
|
+
error: { code: -32e3, message: errorMessage },
|
|
144
|
+
id
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a completed task result from a text response
|
|
150
|
+
*/
|
|
151
|
+
createTaskResult(originalMessage, responseText, contextId) {
|
|
152
|
+
return {
|
|
153
|
+
id: `task-${Date.now()}`,
|
|
154
|
+
contextId: contextId || `ctx-${Date.now()}`,
|
|
155
|
+
status: {
|
|
156
|
+
state: "completed",
|
|
157
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
158
|
+
},
|
|
159
|
+
messages: [
|
|
160
|
+
originalMessage,
|
|
161
|
+
{
|
|
162
|
+
role: "agent",
|
|
163
|
+
parts: [{ kind: "text", text: responseText }],
|
|
164
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Helper to create a text message part
|
|
171
|
+
*/
|
|
172
|
+
static textPart(text) {
|
|
173
|
+
return { kind: "text", text };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Helper to create a file message part
|
|
177
|
+
*/
|
|
178
|
+
static filePart(uri, mimeType) {
|
|
179
|
+
return { kind: "file", uri, mimeType };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Helper to create a data message part (base64)
|
|
183
|
+
*/
|
|
184
|
+
static dataPart(data, mimeType) {
|
|
185
|
+
return { kind: "data", data, mimeType };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
27
189
|
// src/index.ts
|
|
190
|
+
function getTaskResponseText(task) {
|
|
191
|
+
if (task.artifacts?.length) {
|
|
192
|
+
const texts = [];
|
|
193
|
+
for (const artifact of task.artifacts) {
|
|
194
|
+
if (artifact.parts) {
|
|
195
|
+
for (const part of artifact.parts) {
|
|
196
|
+
if (part.kind === "text" && part.text) {
|
|
197
|
+
texts.push(part.text);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (texts.length > 0) {
|
|
203
|
+
return texts.join("\n");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (task.history?.length) {
|
|
207
|
+
const lastMessage = task.history[task.history.length - 1];
|
|
208
|
+
if (lastMessage.parts) {
|
|
209
|
+
const texts = [];
|
|
210
|
+
for (const part of lastMessage.parts) {
|
|
211
|
+
if (part.kind === "text" && part.text) {
|
|
212
|
+
texts.push(part.text);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (texts.length > 0) {
|
|
216
|
+
return texts.join("\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return "";
|
|
221
|
+
}
|
|
28
222
|
var DEFAULT_HUB_URL = "wss://gopherhole.helixdata.workers.dev/ws";
|
|
29
223
|
var GopherHole = class extends EventEmitter {
|
|
30
224
|
constructor(apiKeyOrOptions) {
|
|
@@ -141,6 +335,17 @@ var GopherHole = class extends EventEmitter {
|
|
|
141
335
|
const task = await this.sendText(toAgentId, text, options);
|
|
142
336
|
return this.waitForTask(task.id, options);
|
|
143
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Send a text message and wait for the text response
|
|
340
|
+
* This is a convenience method that extracts the response text automatically
|
|
341
|
+
*/
|
|
342
|
+
async askText(toAgentId, text, options) {
|
|
343
|
+
const task = await this.sendTextAndWait(toAgentId, text, options);
|
|
344
|
+
if (task.status.state === "failed") {
|
|
345
|
+
throw new Error(task.status.message || "Task failed");
|
|
346
|
+
}
|
|
347
|
+
return getTaskResponseText(task);
|
|
348
|
+
}
|
|
144
349
|
/**
|
|
145
350
|
* Wait for a task to complete (polling)
|
|
146
351
|
*/
|
|
@@ -281,7 +486,7 @@ var GopherHole = class extends EventEmitter {
|
|
|
281
486
|
if (this.ws?.readyState === 1) {
|
|
282
487
|
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
283
488
|
}
|
|
284
|
-
},
|
|
489
|
+
}, 15e3);
|
|
285
490
|
}
|
|
286
491
|
/**
|
|
287
492
|
* Stop ping interval
|
|
@@ -474,6 +679,8 @@ var GopherHole = class extends EventEmitter {
|
|
|
474
679
|
var index_default = GopherHole;
|
|
475
680
|
export {
|
|
476
681
|
GopherHole,
|
|
682
|
+
GopherHoleAgent,
|
|
477
683
|
JsonRpcErrorCodes,
|
|
478
|
-
index_default as default
|
|
684
|
+
index_default as default,
|
|
685
|
+
getTaskResponseText
|
|
479
686
|
};
|
package/package.json
CHANGED
package/src/agent.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GopherHoleAgent - Helper for building webhook-based A2A agents
|
|
3
|
+
*
|
|
4
|
+
* Use this to create agents that receive requests from the GopherHole hub
|
|
5
|
+
* via webhooks (e.g., Cloudflare Workers, Express servers).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentCard, AgentSkill } from './types';
|
|
9
|
+
|
|
10
|
+
// Re-export types for convenience
|
|
11
|
+
export type { AgentCard, AgentSkill };
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// TYPES
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
|
+
export interface IncomingMessage {
|
|
18
|
+
role: 'user' | 'agent';
|
|
19
|
+
parts: AgentMessagePart[];
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AgentMessagePart {
|
|
24
|
+
kind: 'text' | 'file' | 'data';
|
|
25
|
+
text?: string;
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
data?: string;
|
|
28
|
+
uri?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentTaskResult {
|
|
32
|
+
id: string;
|
|
33
|
+
contextId: string;
|
|
34
|
+
status: AgentTaskStatus;
|
|
35
|
+
messages: IncomingMessage[];
|
|
36
|
+
artifacts?: AgentArtifact[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AgentTaskStatus {
|
|
40
|
+
state: 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
|
|
41
|
+
timestamp: string;
|
|
42
|
+
message?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AgentArtifact {
|
|
46
|
+
name?: string;
|
|
47
|
+
mimeType?: string;
|
|
48
|
+
parts?: AgentMessagePart[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface JsonRpcRequest {
|
|
52
|
+
jsonrpc: '2.0';
|
|
53
|
+
method: string;
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
id?: string | number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface JsonRpcResponse {
|
|
59
|
+
jsonrpc: '2.0';
|
|
60
|
+
result?: unknown;
|
|
61
|
+
error?: { code: number; message: string; data?: unknown };
|
|
62
|
+
id?: string | number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================
|
|
66
|
+
// MESSAGE HANDLER TYPE
|
|
67
|
+
// ============================================================
|
|
68
|
+
|
|
69
|
+
export interface MessageContext {
|
|
70
|
+
/** The incoming message */
|
|
71
|
+
message: IncomingMessage;
|
|
72
|
+
/** Extracted text content from message parts */
|
|
73
|
+
text: string;
|
|
74
|
+
/** Task ID if provided */
|
|
75
|
+
taskId?: string;
|
|
76
|
+
/** Context ID if provided */
|
|
77
|
+
contextId?: string;
|
|
78
|
+
/** Full params from JSON-RPC request */
|
|
79
|
+
params: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type MessageHandler = (ctx: MessageContext) => Promise<string | AgentTaskResult> | string | AgentTaskResult;
|
|
83
|
+
|
|
84
|
+
// ============================================================
|
|
85
|
+
// AGENT CLASS
|
|
86
|
+
// ============================================================
|
|
87
|
+
|
|
88
|
+
export interface GopherHoleAgentOptions {
|
|
89
|
+
/** Agent card for discovery */
|
|
90
|
+
card: AgentCard;
|
|
91
|
+
/** API key for authentication (from GopherHole hub) */
|
|
92
|
+
apiKey?: string;
|
|
93
|
+
/** Handler for incoming messages */
|
|
94
|
+
onMessage: MessageHandler;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class GopherHoleAgent {
|
|
98
|
+
private card: AgentCard;
|
|
99
|
+
private apiKey?: string;
|
|
100
|
+
private onMessage: MessageHandler;
|
|
101
|
+
|
|
102
|
+
constructor(options: GopherHoleAgentOptions) {
|
|
103
|
+
this.card = options.card;
|
|
104
|
+
this.apiKey = options.apiKey;
|
|
105
|
+
this.onMessage = options.onMessage;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get the agent card */
|
|
109
|
+
getCard(): AgentCard {
|
|
110
|
+
return this.card;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Verify authorization header */
|
|
114
|
+
verifyAuth(authHeader: string | null): boolean {
|
|
115
|
+
if (!this.apiKey) return true; // No auth configured
|
|
116
|
+
return authHeader === `Bearer ${this.apiKey}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle an incoming HTTP request
|
|
121
|
+
* Returns a Response object (works with Cloudflare Workers, Bun, etc.)
|
|
122
|
+
*/
|
|
123
|
+
async handleRequest(request: Request): Promise<Response> {
|
|
124
|
+
const url = new URL(request.url);
|
|
125
|
+
const corsHeaders = {
|
|
126
|
+
'Access-Control-Allow-Origin': '*',
|
|
127
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
128
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// CORS preflight
|
|
132
|
+
if (request.method === 'OPTIONS') {
|
|
133
|
+
return new Response(null, { headers: corsHeaders });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Agent card endpoints
|
|
137
|
+
if (url.pathname === '/.well-known/agent.json' || url.pathname === '/agent.json') {
|
|
138
|
+
return Response.json(this.card, { headers: corsHeaders });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Health check
|
|
142
|
+
if (url.pathname === '/health') {
|
|
143
|
+
return Response.json(
|
|
144
|
+
{ status: 'ok', agent: this.card.name, version: this.card.version || '1.0.0' },
|
|
145
|
+
{ headers: corsHeaders }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// JSON-RPC endpoint
|
|
150
|
+
if (request.method === 'POST' && (url.pathname === '/' || url.pathname === '/a2a')) {
|
|
151
|
+
// Verify auth
|
|
152
|
+
if (this.apiKey && !this.verifyAuth(request.headers.get('Authorization'))) {
|
|
153
|
+
return Response.json(
|
|
154
|
+
{ jsonrpc: '2.0', error: { code: -32001, message: 'Unauthorized' }, id: null },
|
|
155
|
+
{ status: 401, headers: corsHeaders }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const body = await request.json() as JsonRpcRequest;
|
|
161
|
+
const response = await this.handleJsonRpc(body);
|
|
162
|
+
return Response.json(response, { headers: corsHeaders });
|
|
163
|
+
} catch {
|
|
164
|
+
return Response.json(
|
|
165
|
+
{ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null },
|
|
166
|
+
{ status: 400, headers: corsHeaders }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new Response('Not Found', { status: 404, headers: corsHeaders });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle a JSON-RPC request directly
|
|
176
|
+
*/
|
|
177
|
+
async handleJsonRpc(req: JsonRpcRequest): Promise<JsonRpcResponse> {
|
|
178
|
+
const { method, params = {}, id } = req;
|
|
179
|
+
|
|
180
|
+
switch (method) {
|
|
181
|
+
case 'message/send':
|
|
182
|
+
return this.handleMessageSend(params, id);
|
|
183
|
+
|
|
184
|
+
case 'tasks/get':
|
|
185
|
+
return {
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
error: { code: -32601, message: 'This agent does not support persistent tasks' },
|
|
188
|
+
id,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
case 'tasks/cancel':
|
|
192
|
+
return {
|
|
193
|
+
jsonrpc: '2.0',
|
|
194
|
+
error: { code: -32601, message: 'This agent does not support task cancellation' },
|
|
195
|
+
id,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
default:
|
|
199
|
+
return {
|
|
200
|
+
jsonrpc: '2.0',
|
|
201
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
202
|
+
id,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async handleMessageSend(
|
|
208
|
+
params: Record<string, unknown>,
|
|
209
|
+
id?: string | number
|
|
210
|
+
): Promise<JsonRpcResponse> {
|
|
211
|
+
const message = params.message as IncomingMessage | undefined;
|
|
212
|
+
|
|
213
|
+
if (!message?.parts) {
|
|
214
|
+
return {
|
|
215
|
+
jsonrpc: '2.0',
|
|
216
|
+
error: { code: -32602, message: 'Invalid params: message with parts required' },
|
|
217
|
+
id,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Extract text from message parts
|
|
222
|
+
const text = message.parts
|
|
223
|
+
.filter(p => p.kind === 'text' || p.text)
|
|
224
|
+
.map(p => p.text || '')
|
|
225
|
+
.join('\n')
|
|
226
|
+
.trim();
|
|
227
|
+
|
|
228
|
+
const config = params.configuration as Record<string, unknown> | undefined;
|
|
229
|
+
const ctx: MessageContext = {
|
|
230
|
+
message,
|
|
231
|
+
text,
|
|
232
|
+
taskId: params.taskId as string | undefined,
|
|
233
|
+
contextId: config?.contextId as string | undefined,
|
|
234
|
+
params,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const result = await this.onMessage(ctx);
|
|
239
|
+
|
|
240
|
+
// If handler returned a string, wrap it in a task result
|
|
241
|
+
if (typeof result === 'string') {
|
|
242
|
+
const taskResult = this.createTaskResult(message, result, ctx.contextId);
|
|
243
|
+
return { jsonrpc: '2.0', result: taskResult, id };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { jsonrpc: '2.0', result, id };
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const errorMessage = error instanceof Error ? error.message : 'Handler error';
|
|
249
|
+
return {
|
|
250
|
+
jsonrpc: '2.0',
|
|
251
|
+
error: { code: -32000, message: errorMessage },
|
|
252
|
+
id,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create a completed task result from a text response
|
|
259
|
+
*/
|
|
260
|
+
createTaskResult(
|
|
261
|
+
originalMessage: IncomingMessage,
|
|
262
|
+
responseText: string,
|
|
263
|
+
contextId?: string
|
|
264
|
+
): AgentTaskResult {
|
|
265
|
+
return {
|
|
266
|
+
id: `task-${Date.now()}`,
|
|
267
|
+
contextId: contextId || `ctx-${Date.now()}`,
|
|
268
|
+
status: {
|
|
269
|
+
state: 'completed',
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
},
|
|
272
|
+
messages: [
|
|
273
|
+
originalMessage,
|
|
274
|
+
{
|
|
275
|
+
role: 'agent',
|
|
276
|
+
parts: [{ kind: 'text', text: responseText }],
|
|
277
|
+
metadata: { generatedAt: new Date().toISOString() },
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Helper to create a text message part
|
|
285
|
+
*/
|
|
286
|
+
static textPart(text: string): AgentMessagePart {
|
|
287
|
+
return { kind: 'text', text };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Helper to create a file message part
|
|
292
|
+
*/
|
|
293
|
+
static filePart(uri: string, mimeType: string): AgentMessagePart {
|
|
294
|
+
return { kind: 'file', uri, mimeType };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Helper to create a data message part (base64)
|
|
299
|
+
*/
|
|
300
|
+
static dataPart(data: string, mimeType: string): AgentMessagePart {
|
|
301
|
+
return { kind: 'data', data, mimeType };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default GopherHoleAgent;
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,22 @@ import { EventEmitter } from 'eventemitter3';
|
|
|
3
3
|
// Re-export types
|
|
4
4
|
export * from './types';
|
|
5
5
|
|
|
6
|
+
// Re-export agent helper for webhook-based agents
|
|
7
|
+
export {
|
|
8
|
+
GopherHoleAgent,
|
|
9
|
+
GopherHoleAgentOptions,
|
|
10
|
+
IncomingMessage,
|
|
11
|
+
AgentMessagePart,
|
|
12
|
+
AgentTaskResult,
|
|
13
|
+
AgentTaskStatus,
|
|
14
|
+
AgentArtifact,
|
|
15
|
+
MessageContext,
|
|
16
|
+
MessageHandler,
|
|
17
|
+
} from './agent';
|
|
18
|
+
|
|
19
|
+
// Convenience type aliases
|
|
20
|
+
export type { AgentTaskResult as TaskResult } from './agent';
|
|
21
|
+
|
|
6
22
|
export interface GopherHoleOptions {
|
|
7
23
|
/** API key (starts with gph_) */
|
|
8
24
|
apiKey: string;
|
|
@@ -75,12 +91,55 @@ export interface TaskStatus {
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
export interface Artifact {
|
|
78
|
-
name
|
|
79
|
-
|
|
94
|
+
name?: string;
|
|
95
|
+
artifactId?: string;
|
|
96
|
+
mimeType?: string;
|
|
97
|
+
parts?: MessagePart[];
|
|
80
98
|
data?: string;
|
|
81
99
|
uri?: string;
|
|
82
100
|
}
|
|
83
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Extract text response from a completed task.
|
|
104
|
+
* Checks artifacts first (where responses live), then falls back to history.
|
|
105
|
+
*/
|
|
106
|
+
export function getTaskResponseText(task: Task): string {
|
|
107
|
+
// Check artifacts first (this is where responses from other agents appear)
|
|
108
|
+
if (task.artifacts?.length) {
|
|
109
|
+
const texts: string[] = [];
|
|
110
|
+
for (const artifact of task.artifacts) {
|
|
111
|
+
if (artifact.parts) {
|
|
112
|
+
for (const part of artifact.parts) {
|
|
113
|
+
if (part.kind === 'text' && part.text) {
|
|
114
|
+
texts.push(part.text);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (texts.length > 0) {
|
|
120
|
+
return texts.join('\n');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fall back to history (last message)
|
|
125
|
+
if (task.history?.length) {
|
|
126
|
+
const lastMessage = task.history[task.history.length - 1];
|
|
127
|
+
if (lastMessage.parts) {
|
|
128
|
+
const texts: string[] = [];
|
|
129
|
+
for (const part of lastMessage.parts) {
|
|
130
|
+
if (part.kind === 'text' && part.text) {
|
|
131
|
+
texts.push(part.text);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (texts.length > 0) {
|
|
135
|
+
return texts.join('\n');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
|
|
84
143
|
export interface SendOptions {
|
|
85
144
|
/** Existing context/conversation ID */
|
|
86
145
|
contextId?: string;
|
|
@@ -253,6 +312,18 @@ export class GopherHole extends EventEmitter<EventMap> {
|
|
|
253
312
|
return this.waitForTask(task.id, options);
|
|
254
313
|
}
|
|
255
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Send a text message and wait for the text response
|
|
317
|
+
* This is a convenience method that extracts the response text automatically
|
|
318
|
+
*/
|
|
319
|
+
async askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string> {
|
|
320
|
+
const task = await this.sendTextAndWait(toAgentId, text, options);
|
|
321
|
+
if (task.status.state === 'failed') {
|
|
322
|
+
throw new Error(task.status.message || 'Task failed');
|
|
323
|
+
}
|
|
324
|
+
return getTaskResponseText(task);
|
|
325
|
+
}
|
|
326
|
+
|
|
256
327
|
/**
|
|
257
328
|
* Wait for a task to complete (polling)
|
|
258
329
|
*/
|
|
@@ -425,7 +496,7 @@ export class GopherHole extends EventEmitter<EventMap> {
|
|
|
425
496
|
if (this.ws?.readyState === 1) { // OPEN
|
|
426
497
|
this.ws.send(JSON.stringify({ type: 'ping' }));
|
|
427
498
|
}
|
|
428
|
-
},
|
|
499
|
+
}, 15000); // 15s ping to keep connection alive
|
|
429
500
|
}
|
|
430
501
|
|
|
431
502
|
/**
|