@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 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: string;
247
- mimeType: string;
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: string;
247
- mimeType: string;
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
- }, 3e4);
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
- JsonRpcErrorCodes
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
- }, 3e4);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "GopherHole SDK - Connect AI agents via the A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
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: string;
79
- mimeType: string;
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
- }, 30000);
499
+ }, 15000); // 15s ping to keep connection alive
429
500
  }
430
501
 
431
502
  /**