@gopherhole/sdk 0.1.4 → 0.2.2

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/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;
@@ -194,6 +304,8 @@ interface GopherHoleOptions {
194
304
  maxReconnectAttempts?: number;
195
305
  /** Default request timeout in ms (default: 30000) */
196
306
  requestTimeout?: number;
307
+ /** Default message response timeout in ms (default: 30000) */
308
+ messageTimeout?: number;
197
309
  }
198
310
  /** Agent card configuration for registration */
199
311
  interface AgentCardConfig {
@@ -287,12 +399,17 @@ declare class GopherHole extends EventEmitter<EventMap> {
287
399
  private reconnectDelay;
288
400
  private maxReconnectAttempts;
289
401
  private requestTimeout;
402
+ private messageTimeout;
290
403
  private reconnectAttempts;
291
404
  private reconnectTimer;
292
405
  private pingInterval;
293
406
  private agentId;
294
407
  private agentCard;
295
408
  constructor(apiKeyOrOptions: string | GopherHoleOptions);
409
+ /**
410
+ * Get the configured message timeout
411
+ */
412
+ getMessageTimeout(): number;
296
413
  /**
297
414
  * Update agent card (sends to hub if connected)
298
415
  */
@@ -347,12 +464,28 @@ declare class GopherHole extends EventEmitter<EventMap> {
347
464
  * Cancel a task
348
465
  */
349
466
  cancelTask(taskId: string): Promise<Task>;
467
+ /**
468
+ * Respond to an incoming task via WebSocket (completes the task)
469
+ * Use this when you receive a 'message' event and want to send back a response
470
+ * that completes the original task.
471
+ */
472
+ respond(taskId: string, text: string, options?: {
473
+ status?: 'completed' | 'failed';
474
+ message?: string;
475
+ }): void;
476
+ /**
477
+ * Respond with a failure to an incoming task
478
+ */
479
+ respondError(taskId: string, errorMessage: string): void;
350
480
  /**
351
481
  * Reply to a message/task (sends back to the original caller)
482
+ * Note: This creates a NEW task via HTTP. For completing an existing task,
483
+ * use respond() instead.
352
484
  */
353
485
  reply(taskId: string, payload: MessagePayload, toAgentId?: string): Promise<Task>;
354
486
  /**
355
- * Reply with text
487
+ * Reply with text (creates new task)
488
+ * Note: For completing an existing task, use respond() instead.
356
489
  */
357
490
  replyText(taskId: string, text: string): Promise<Task>;
358
491
  /**
@@ -534,4 +667,4 @@ interface RatingResult {
534
667
  ratingCount: number;
535
668
  }
536
669
 
537
- 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, getTaskResponseText };
670
+ 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;
@@ -194,6 +304,8 @@ interface GopherHoleOptions {
194
304
  maxReconnectAttempts?: number;
195
305
  /** Default request timeout in ms (default: 30000) */
196
306
  requestTimeout?: number;
307
+ /** Default message response timeout in ms (default: 30000) */
308
+ messageTimeout?: number;
197
309
  }
198
310
  /** Agent card configuration for registration */
199
311
  interface AgentCardConfig {
@@ -287,12 +399,17 @@ declare class GopherHole extends EventEmitter<EventMap> {
287
399
  private reconnectDelay;
288
400
  private maxReconnectAttempts;
289
401
  private requestTimeout;
402
+ private messageTimeout;
290
403
  private reconnectAttempts;
291
404
  private reconnectTimer;
292
405
  private pingInterval;
293
406
  private agentId;
294
407
  private agentCard;
295
408
  constructor(apiKeyOrOptions: string | GopherHoleOptions);
409
+ /**
410
+ * Get the configured message timeout
411
+ */
412
+ getMessageTimeout(): number;
296
413
  /**
297
414
  * Update agent card (sends to hub if connected)
298
415
  */
@@ -347,12 +464,28 @@ declare class GopherHole extends EventEmitter<EventMap> {
347
464
  * Cancel a task
348
465
  */
349
466
  cancelTask(taskId: string): Promise<Task>;
467
+ /**
468
+ * Respond to an incoming task via WebSocket (completes the task)
469
+ * Use this when you receive a 'message' event and want to send back a response
470
+ * that completes the original task.
471
+ */
472
+ respond(taskId: string, text: string, options?: {
473
+ status?: 'completed' | 'failed';
474
+ message?: string;
475
+ }): void;
476
+ /**
477
+ * Respond with a failure to an incoming task
478
+ */
479
+ respondError(taskId: string, errorMessage: string): void;
350
480
  /**
351
481
  * Reply to a message/task (sends back to the original caller)
482
+ * Note: This creates a NEW task via HTTP. For completing an existing task,
483
+ * use respond() instead.
352
484
  */
353
485
  reply(taskId: string, payload: MessagePayload, toAgentId?: string): Promise<Task>;
354
486
  /**
355
- * Reply with text
487
+ * Reply with text (creates new task)
488
+ * Note: For completing an existing task, use respond() instead.
356
489
  */
357
490
  replyText(taskId: string, text: string): Promise<Task>;
358
491
  /**
@@ -534,4 +667,4 @@ interface RatingResult {
534
667
  ratingCount: number;
535
668
  }
536
669
 
537
- 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, getTaskResponseText };
670
+ 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,6 +21,7 @@ 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
26
  default: () => index_default,
26
27
  getTaskResponseText: () => getTaskResponseText
@@ -44,6 +45,168 @@ var JsonRpcErrorCodes = {
44
45
  InvalidAgentCard: -32006
45
46
  };
46
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
+
47
210
  // src/index.ts
48
211
  function getTaskResponseText(task) {
49
212
  if (task.artifacts?.length) {
@@ -96,6 +259,13 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
96
259
  this.reconnectDelay = options.reconnectDelay ?? 1e3;
97
260
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
98
261
  this.requestTimeout = options.requestTimeout ?? 3e4;
262
+ this.messageTimeout = options.messageTimeout ?? 3e4;
263
+ }
264
+ /**
265
+ * Get the configured message timeout
266
+ */
267
+ getMessageTimeout() {
268
+ return this.messageTimeout;
99
269
  }
100
270
  /**
101
271
  * Update agent card (sends to hub if connected)
@@ -244,8 +414,41 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
244
414
  const response = await this.rpc("tasks/cancel", { id: taskId });
245
415
  return response;
246
416
  }
417
+ /**
418
+ * Respond to an incoming task via WebSocket (completes the task)
419
+ * Use this when you receive a 'message' event and want to send back a response
420
+ * that completes the original task.
421
+ */
422
+ respond(taskId, text, options) {
423
+ if (!this.ws || this.ws.readyState !== 1) {
424
+ throw new Error("WebSocket not connected");
425
+ }
426
+ const response = {
427
+ type: "task_response",
428
+ taskId,
429
+ status: {
430
+ state: options?.status ?? "completed",
431
+ message: options?.message
432
+ },
433
+ artifact: {
434
+ artifactId: `response-${Date.now()}`,
435
+ mimeType: "text/plain",
436
+ parts: [{ kind: "text", text }]
437
+ },
438
+ lastChunk: true
439
+ };
440
+ this.ws.send(JSON.stringify(response));
441
+ }
442
+ /**
443
+ * Respond with a failure to an incoming task
444
+ */
445
+ respondError(taskId, errorMessage) {
446
+ this.respond(taskId, errorMessage, { status: "failed", message: errorMessage });
447
+ }
247
448
  /**
248
449
  * Reply to a message/task (sends back to the original caller)
450
+ * Note: This creates a NEW task via HTTP. For completing an existing task,
451
+ * use respond() instead.
249
452
  */
250
453
  async reply(taskId, payload, toAgentId) {
251
454
  if (!toAgentId) {
@@ -268,7 +471,8 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
268
471
  return response;
269
472
  }
270
473
  /**
271
- * Reply with text
474
+ * Reply with text (creates new task)
475
+ * Note: For completing an existing task, use respond() instead.
272
476
  */
273
477
  async replyText(taskId, text) {
274
478
  return this.reply(taskId, {
@@ -344,7 +548,7 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
344
548
  if (this.ws?.readyState === 1) {
345
549
  this.ws.send(JSON.stringify({ type: "ping" }));
346
550
  }
347
- }, 3e4);
551
+ }, 15e3);
348
552
  }
349
553
  /**
350
554
  * Stop ping interval
@@ -538,6 +742,7 @@ var index_default = GopherHole;
538
742
  // Annotate the CommonJS export names for ESM import in node:
539
743
  0 && (module.exports = {
540
744
  GopherHole,
745
+ GopherHoleAgent,
541
746
  JsonRpcErrorCodes,
542
747
  getTaskResponseText
543
748
  });
package/dist/index.mjs CHANGED
@@ -24,6 +24,168 @@ 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
28
190
  function getTaskResponseText(task) {
29
191
  if (task.artifacts?.length) {
@@ -76,6 +238,13 @@ var GopherHole = class extends EventEmitter {
76
238
  this.reconnectDelay = options.reconnectDelay ?? 1e3;
77
239
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
78
240
  this.requestTimeout = options.requestTimeout ?? 3e4;
241
+ this.messageTimeout = options.messageTimeout ?? 3e4;
242
+ }
243
+ /**
244
+ * Get the configured message timeout
245
+ */
246
+ getMessageTimeout() {
247
+ return this.messageTimeout;
79
248
  }
80
249
  /**
81
250
  * Update agent card (sends to hub if connected)
@@ -224,8 +393,41 @@ var GopherHole = class extends EventEmitter {
224
393
  const response = await this.rpc("tasks/cancel", { id: taskId });
225
394
  return response;
226
395
  }
396
+ /**
397
+ * Respond to an incoming task via WebSocket (completes the task)
398
+ * Use this when you receive a 'message' event and want to send back a response
399
+ * that completes the original task.
400
+ */
401
+ respond(taskId, text, options) {
402
+ if (!this.ws || this.ws.readyState !== 1) {
403
+ throw new Error("WebSocket not connected");
404
+ }
405
+ const response = {
406
+ type: "task_response",
407
+ taskId,
408
+ status: {
409
+ state: options?.status ?? "completed",
410
+ message: options?.message
411
+ },
412
+ artifact: {
413
+ artifactId: `response-${Date.now()}`,
414
+ mimeType: "text/plain",
415
+ parts: [{ kind: "text", text }]
416
+ },
417
+ lastChunk: true
418
+ };
419
+ this.ws.send(JSON.stringify(response));
420
+ }
421
+ /**
422
+ * Respond with a failure to an incoming task
423
+ */
424
+ respondError(taskId, errorMessage) {
425
+ this.respond(taskId, errorMessage, { status: "failed", message: errorMessage });
426
+ }
227
427
  /**
228
428
  * Reply to a message/task (sends back to the original caller)
429
+ * Note: This creates a NEW task via HTTP. For completing an existing task,
430
+ * use respond() instead.
229
431
  */
230
432
  async reply(taskId, payload, toAgentId) {
231
433
  if (!toAgentId) {
@@ -248,7 +450,8 @@ var GopherHole = class extends EventEmitter {
248
450
  return response;
249
451
  }
250
452
  /**
251
- * Reply with text
453
+ * Reply with text (creates new task)
454
+ * Note: For completing an existing task, use respond() instead.
252
455
  */
253
456
  async replyText(taskId, text) {
254
457
  return this.reply(taskId, {
@@ -324,7 +527,7 @@ var GopherHole = class extends EventEmitter {
324
527
  if (this.ws?.readyState === 1) {
325
528
  this.ws.send(JSON.stringify({ type: "ping" }));
326
529
  }
327
- }, 3e4);
530
+ }, 15e3);
328
531
  }
329
532
  /**
330
533
  * Stop ping interval
@@ -517,6 +720,7 @@ var GopherHole = class extends EventEmitter {
517
720
  var index_default = GopherHole;
518
721
  export {
519
722
  GopherHole,
723
+ GopherHoleAgent,
520
724
  JsonRpcErrorCodes,
521
725
  index_default as default,
522
726
  getTaskResponseText
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/sdk",
3
- "version": "0.1.4",
3
+ "version": "0.2.2",
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;
@@ -18,6 +34,8 @@ export interface GopherHoleOptions {
18
34
  maxReconnectAttempts?: number;
19
35
  /** Default request timeout in ms (default: 30000) */
20
36
  requestTimeout?: number;
37
+ /** Default message response timeout in ms (default: 30000) */
38
+ messageTimeout?: number;
21
39
  }
22
40
 
23
41
  /** Agent card configuration for registration */
@@ -162,6 +180,7 @@ export class GopherHole extends EventEmitter<EventMap> {
162
180
  private reconnectDelay: number;
163
181
  private maxReconnectAttempts: number;
164
182
  private requestTimeout: number;
183
+ private messageTimeout: number;
165
184
  private reconnectAttempts = 0;
166
185
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
167
186
  private pingInterval: ReturnType<typeof setInterval> | null = null;
@@ -183,6 +202,14 @@ export class GopherHole extends EventEmitter<EventMap> {
183
202
  this.reconnectDelay = options.reconnectDelay ?? 1000;
184
203
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
185
204
  this.requestTimeout = options.requestTimeout ?? 30000;
205
+ this.messageTimeout = options.messageTimeout ?? 30000;
206
+ }
207
+
208
+ /**
209
+ * Get the configured message timeout
210
+ */
211
+ getMessageTimeout(): number {
212
+ return this.messageTimeout;
186
213
  }
187
214
 
188
215
  /**
@@ -362,8 +389,45 @@ export class GopherHole extends EventEmitter<EventMap> {
362
389
  return response as Task;
363
390
  }
364
391
 
392
+ /**
393
+ * Respond to an incoming task via WebSocket (completes the task)
394
+ * Use this when you receive a 'message' event and want to send back a response
395
+ * that completes the original task.
396
+ */
397
+ respond(taskId: string, text: string, options?: { status?: 'completed' | 'failed'; message?: string }): void {
398
+ if (!this.ws || this.ws.readyState !== 1) {
399
+ throw new Error('WebSocket not connected');
400
+ }
401
+
402
+ const response = {
403
+ type: 'task_response',
404
+ taskId,
405
+ status: {
406
+ state: options?.status ?? 'completed',
407
+ message: options?.message,
408
+ },
409
+ artifact: {
410
+ artifactId: `response-${Date.now()}`,
411
+ mimeType: 'text/plain',
412
+ parts: [{ kind: 'text', text }],
413
+ },
414
+ lastChunk: true,
415
+ };
416
+
417
+ this.ws.send(JSON.stringify(response));
418
+ }
419
+
420
+ /**
421
+ * Respond with a failure to an incoming task
422
+ */
423
+ respondError(taskId: string, errorMessage: string): void {
424
+ this.respond(taskId, errorMessage, { status: 'failed', message: errorMessage });
425
+ }
426
+
365
427
  /**
366
428
  * Reply to a message/task (sends back to the original caller)
429
+ * Note: This creates a NEW task via HTTP. For completing an existing task,
430
+ * use respond() instead.
367
431
  */
368
432
  async reply(taskId: string, payload: MessagePayload, toAgentId?: string): Promise<Task> {
369
433
  // If toAgentId not provided, we need to figure out who to reply to
@@ -394,7 +458,8 @@ export class GopherHole extends EventEmitter<EventMap> {
394
458
  }
395
459
 
396
460
  /**
397
- * Reply with text
461
+ * Reply with text (creates new task)
462
+ * Note: For completing an existing task, use respond() instead.
398
463
  */
399
464
  async replyText(taskId: string, text: string): Promise<Task> {
400
465
  return this.reply(taskId, {
@@ -480,7 +545,7 @@ export class GopherHole extends EventEmitter<EventMap> {
480
545
  if (this.ws?.readyState === 1) { // OPEN
481
546
  this.ws.send(JSON.stringify({ type: 'ping' }));
482
547
  }
483
- }, 30000);
548
+ }, 15000); // 15s ping to keep connection alive
484
549
  }
485
550
 
486
551
  /**