@gopherhole/sdk 0.1.2 → 0.1.4

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
@@ -192,6 +192,8 @@ interface GopherHoleOptions {
192
192
  reconnectDelay?: number;
193
193
  /** Max reconnect attempts */
194
194
  maxReconnectAttempts?: number;
195
+ /** Default request timeout in ms (default: 30000) */
196
+ requestTimeout?: number;
195
197
  }
196
198
  /** Agent card configuration for registration */
197
199
  interface AgentCardConfig {
@@ -241,11 +243,18 @@ interface TaskStatus {
241
243
  message?: string;
242
244
  }
243
245
  interface Artifact {
244
- name: string;
245
- mimeType: string;
246
+ name?: string;
247
+ artifactId?: string;
248
+ mimeType?: string;
249
+ parts?: MessagePart[];
246
250
  data?: string;
247
251
  uri?: string;
248
252
  }
253
+ /**
254
+ * Extract text response from a completed task.
255
+ * Checks artifacts first (where responses live), then falls back to history.
256
+ */
257
+ declare function getTaskResponseText(task: Task): string;
249
258
  interface SendOptions {
250
259
  /** Existing context/conversation ID */
251
260
  contextId?: string;
@@ -253,6 +262,14 @@ interface SendOptions {
253
262
  pushNotificationUrl?: string;
254
263
  /** History length to include */
255
264
  historyLength?: number;
265
+ /** Request timeout in ms (overrides default) */
266
+ timeoutMs?: number;
267
+ }
268
+ interface SendAndWaitOptions extends SendOptions {
269
+ /** Polling interval in ms (default: 1000) */
270
+ pollIntervalMs?: number;
271
+ /** Max wait time in ms (default: 300000 = 5 min) */
272
+ maxWaitMs?: number;
256
273
  }
257
274
  type EventMap = {
258
275
  connect: () => void;
@@ -269,6 +286,7 @@ declare class GopherHole extends EventEmitter<EventMap> {
269
286
  private autoReconnect;
270
287
  private reconnectDelay;
271
288
  private maxReconnectAttempts;
289
+ private requestTimeout;
272
290
  private reconnectAttempts;
273
291
  private reconnectTimer;
274
292
  private pingInterval;
@@ -295,6 +313,20 @@ declare class GopherHole extends EventEmitter<EventMap> {
295
313
  * Send a text message to another agent
296
314
  */
297
315
  sendText(toAgentId: string, text: string, options?: SendOptions): Promise<Task>;
316
+ /**
317
+ * Send a text message and wait for completion
318
+ * Returns the completed task with response artifacts
319
+ */
320
+ sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task>;
321
+ /**
322
+ * Send a text message and wait for the text response
323
+ * This is a convenience method that extracts the response text automatically
324
+ */
325
+ askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string>;
326
+ /**
327
+ * Wait for a task to complete (polling)
328
+ */
329
+ waitForTask(taskId: string, options?: SendAndWaitOptions): Promise<Task>;
298
330
  /**
299
331
  * Get a task by ID
300
332
  */
@@ -502,4 +534,4 @@ interface RatingResult {
502
534
  ratingCount: number;
503
535
  }
504
536
 
505
- 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 SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default };
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 };
package/dist/index.d.ts CHANGED
@@ -192,6 +192,8 @@ interface GopherHoleOptions {
192
192
  reconnectDelay?: number;
193
193
  /** Max reconnect attempts */
194
194
  maxReconnectAttempts?: number;
195
+ /** Default request timeout in ms (default: 30000) */
196
+ requestTimeout?: number;
195
197
  }
196
198
  /** Agent card configuration for registration */
197
199
  interface AgentCardConfig {
@@ -241,11 +243,18 @@ interface TaskStatus {
241
243
  message?: string;
242
244
  }
243
245
  interface Artifact {
244
- name: string;
245
- mimeType: string;
246
+ name?: string;
247
+ artifactId?: string;
248
+ mimeType?: string;
249
+ parts?: MessagePart[];
246
250
  data?: string;
247
251
  uri?: string;
248
252
  }
253
+ /**
254
+ * Extract text response from a completed task.
255
+ * Checks artifacts first (where responses live), then falls back to history.
256
+ */
257
+ declare function getTaskResponseText(task: Task): string;
249
258
  interface SendOptions {
250
259
  /** Existing context/conversation ID */
251
260
  contextId?: string;
@@ -253,6 +262,14 @@ interface SendOptions {
253
262
  pushNotificationUrl?: string;
254
263
  /** History length to include */
255
264
  historyLength?: number;
265
+ /** Request timeout in ms (overrides default) */
266
+ timeoutMs?: number;
267
+ }
268
+ interface SendAndWaitOptions extends SendOptions {
269
+ /** Polling interval in ms (default: 1000) */
270
+ pollIntervalMs?: number;
271
+ /** Max wait time in ms (default: 300000 = 5 min) */
272
+ maxWaitMs?: number;
256
273
  }
257
274
  type EventMap = {
258
275
  connect: () => void;
@@ -269,6 +286,7 @@ declare class GopherHole extends EventEmitter<EventMap> {
269
286
  private autoReconnect;
270
287
  private reconnectDelay;
271
288
  private maxReconnectAttempts;
289
+ private requestTimeout;
272
290
  private reconnectAttempts;
273
291
  private reconnectTimer;
274
292
  private pingInterval;
@@ -295,6 +313,20 @@ declare class GopherHole extends EventEmitter<EventMap> {
295
313
  * Send a text message to another agent
296
314
  */
297
315
  sendText(toAgentId: string, text: string, options?: SendOptions): Promise<Task>;
316
+ /**
317
+ * Send a text message and wait for completion
318
+ * Returns the completed task with response artifacts
319
+ */
320
+ sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task>;
321
+ /**
322
+ * Send a text message and wait for the text response
323
+ * This is a convenience method that extracts the response text automatically
324
+ */
325
+ askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string>;
326
+ /**
327
+ * Wait for a task to complete (polling)
328
+ */
329
+ waitForTask(taskId: string, options?: SendAndWaitOptions): Promise<Task>;
298
330
  /**
299
331
  * Get a task by ID
300
332
  */
@@ -502,4 +534,4 @@ interface RatingResult {
502
534
  ratingCount: number;
503
535
  }
504
536
 
505
- 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 SendOptions, type Task, type TaskArtifactUpdateEvent, type TaskEvent, type TaskListConfiguration, type TaskPushNotificationConfig, type TaskQueryConfiguration, type TaskState, type TaskStatus, type TaskStatusUpdateEvent, type TextPart, GopherHole as default };
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 };
package/dist/index.js CHANGED
@@ -22,7 +22,8 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  GopherHole: () => GopherHole,
24
24
  JsonRpcErrorCodes: () => JsonRpcErrorCodes,
25
- default: () => index_default
25
+ default: () => index_default,
26
+ getTaskResponseText: () => getTaskResponseText
26
27
  });
27
28
  module.exports = __toCommonJS(index_exports);
28
29
  var import_eventemitter3 = require("eventemitter3");
@@ -44,6 +45,38 @@ var JsonRpcErrorCodes = {
44
45
  };
45
46
 
46
47
  // src/index.ts
48
+ function getTaskResponseText(task) {
49
+ if (task.artifacts?.length) {
50
+ const texts = [];
51
+ for (const artifact of task.artifacts) {
52
+ if (artifact.parts) {
53
+ for (const part of artifact.parts) {
54
+ if (part.kind === "text" && part.text) {
55
+ texts.push(part.text);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ if (texts.length > 0) {
61
+ return texts.join("\n");
62
+ }
63
+ }
64
+ if (task.history?.length) {
65
+ const lastMessage = task.history[task.history.length - 1];
66
+ if (lastMessage.parts) {
67
+ const texts = [];
68
+ for (const part of lastMessage.parts) {
69
+ if (part.kind === "text" && part.text) {
70
+ texts.push(part.text);
71
+ }
72
+ }
73
+ if (texts.length > 0) {
74
+ return texts.join("\n");
75
+ }
76
+ }
77
+ }
78
+ return "";
79
+ }
47
80
  var DEFAULT_HUB_URL = "wss://gopherhole.helixdata.workers.dev/ws";
48
81
  var GopherHole = class extends import_eventemitter3.EventEmitter {
49
82
  constructor(apiKeyOrOptions) {
@@ -62,6 +95,7 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
62
95
  this.autoReconnect = options.autoReconnect ?? true;
63
96
  this.reconnectDelay = options.reconnectDelay ?? 1e3;
64
97
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
98
+ this.requestTimeout = options.requestTimeout ?? 3e4;
65
99
  }
66
100
  /**
67
101
  * Update agent card (sends to hub if connected)
@@ -132,13 +166,14 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
132
166
  * Send a message to another agent
133
167
  */
134
168
  async send(toAgentId, payload, options) {
169
+ const { timeoutMs, ...config } = options || {};
135
170
  const response = await this.rpc("message/send", {
136
171
  message: payload,
137
172
  configuration: {
138
173
  agentId: toAgentId,
139
- ...options
174
+ ...config
140
175
  }
141
- });
176
+ }, timeoutMs);
142
177
  return response;
143
178
  }
144
179
  /**
@@ -150,6 +185,41 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
150
185
  parts: [{ kind: "text", text }]
151
186
  }, options);
152
187
  }
188
+ /**
189
+ * Send a text message and wait for completion
190
+ * Returns the completed task with response artifacts
191
+ */
192
+ async sendTextAndWait(toAgentId, text, options) {
193
+ const task = await this.sendText(toAgentId, text, options);
194
+ return this.waitForTask(task.id, options);
195
+ }
196
+ /**
197
+ * Send a text message and wait for the text response
198
+ * This is a convenience method that extracts the response text automatically
199
+ */
200
+ async askText(toAgentId, text, options) {
201
+ const task = await this.sendTextAndWait(toAgentId, text, options);
202
+ if (task.status.state === "failed") {
203
+ throw new Error(task.status.message || "Task failed");
204
+ }
205
+ return getTaskResponseText(task);
206
+ }
207
+ /**
208
+ * Wait for a task to complete (polling)
209
+ */
210
+ async waitForTask(taskId, options) {
211
+ const pollInterval = options?.pollIntervalMs ?? 1e3;
212
+ const maxWait = options?.maxWaitMs ?? 3e5;
213
+ const startTime = Date.now();
214
+ while (Date.now() - startTime < maxWait) {
215
+ const task = await this.getTask(taskId);
216
+ if (task.status.state === "completed" || task.status.state === "failed" || task.status.state === "canceled" || task.status.state === "rejected") {
217
+ return task;
218
+ }
219
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
220
+ }
221
+ throw new Error(`Task ${taskId} did not complete within ${maxWait}ms`);
222
+ }
153
223
  /**
154
224
  * Get a task by ID
155
225
  */
@@ -209,25 +279,38 @@ var GopherHole = class extends import_eventemitter3.EventEmitter {
209
279
  /**
210
280
  * Make a JSON-RPC call to the A2A endpoint
211
281
  */
212
- async rpc(method, params) {
213
- const response = await fetch(`${this.apiUrl}/a2a`, {
214
- method: "POST",
215
- headers: {
216
- "Content-Type": "application/json",
217
- "Authorization": `Bearer ${this.apiKey}`
218
- },
219
- body: JSON.stringify({
220
- jsonrpc: "2.0",
221
- method,
222
- params,
223
- id: Date.now()
224
- })
225
- });
226
- const data = await response.json();
227
- if (data.error) {
228
- throw new Error(data.error.message || "RPC error");
282
+ async rpc(method, params, timeoutMs) {
283
+ const timeout = timeoutMs ?? this.requestTimeout;
284
+ const controller = new AbortController();
285
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
286
+ try {
287
+ const response = await fetch(`${this.apiUrl}/a2a`, {
288
+ method: "POST",
289
+ headers: {
290
+ "Content-Type": "application/json",
291
+ "Authorization": `Bearer ${this.apiKey}`
292
+ },
293
+ body: JSON.stringify({
294
+ jsonrpc: "2.0",
295
+ method,
296
+ params,
297
+ id: Date.now()
298
+ }),
299
+ signal: controller.signal
300
+ });
301
+ const data = await response.json();
302
+ if (data.error) {
303
+ throw new Error(data.error.message || "RPC error");
304
+ }
305
+ return data.result;
306
+ } catch (err) {
307
+ if (err instanceof Error && err.name === "AbortError") {
308
+ throw new Error(`Request timeout after ${timeout}ms`);
309
+ }
310
+ throw err;
311
+ } finally {
312
+ clearTimeout(timeoutId);
229
313
  }
230
- return data.result;
231
314
  }
232
315
  /**
233
316
  * Handle incoming WebSocket messages
@@ -455,5 +538,6 @@ var index_default = GopherHole;
455
538
  // Annotate the CommonJS export names for ESM import in node:
456
539
  0 && (module.exports = {
457
540
  GopherHole,
458
- JsonRpcErrorCodes
541
+ JsonRpcErrorCodes,
542
+ getTaskResponseText
459
543
  });
package/dist/index.mjs CHANGED
@@ -25,6 +25,38 @@ var JsonRpcErrorCodes = {
25
25
  };
26
26
 
27
27
  // src/index.ts
28
+ function getTaskResponseText(task) {
29
+ if (task.artifacts?.length) {
30
+ const texts = [];
31
+ for (const artifact of task.artifacts) {
32
+ if (artifact.parts) {
33
+ for (const part of artifact.parts) {
34
+ if (part.kind === "text" && part.text) {
35
+ texts.push(part.text);
36
+ }
37
+ }
38
+ }
39
+ }
40
+ if (texts.length > 0) {
41
+ return texts.join("\n");
42
+ }
43
+ }
44
+ if (task.history?.length) {
45
+ const lastMessage = task.history[task.history.length - 1];
46
+ if (lastMessage.parts) {
47
+ const texts = [];
48
+ for (const part of lastMessage.parts) {
49
+ if (part.kind === "text" && part.text) {
50
+ texts.push(part.text);
51
+ }
52
+ }
53
+ if (texts.length > 0) {
54
+ return texts.join("\n");
55
+ }
56
+ }
57
+ }
58
+ return "";
59
+ }
28
60
  var DEFAULT_HUB_URL = "wss://gopherhole.helixdata.workers.dev/ws";
29
61
  var GopherHole = class extends EventEmitter {
30
62
  constructor(apiKeyOrOptions) {
@@ -43,6 +75,7 @@ var GopherHole = class extends EventEmitter {
43
75
  this.autoReconnect = options.autoReconnect ?? true;
44
76
  this.reconnectDelay = options.reconnectDelay ?? 1e3;
45
77
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
78
+ this.requestTimeout = options.requestTimeout ?? 3e4;
46
79
  }
47
80
  /**
48
81
  * Update agent card (sends to hub if connected)
@@ -113,13 +146,14 @@ var GopherHole = class extends EventEmitter {
113
146
  * Send a message to another agent
114
147
  */
115
148
  async send(toAgentId, payload, options) {
149
+ const { timeoutMs, ...config } = options || {};
116
150
  const response = await this.rpc("message/send", {
117
151
  message: payload,
118
152
  configuration: {
119
153
  agentId: toAgentId,
120
- ...options
154
+ ...config
121
155
  }
122
- });
156
+ }, timeoutMs);
123
157
  return response;
124
158
  }
125
159
  /**
@@ -131,6 +165,41 @@ var GopherHole = class extends EventEmitter {
131
165
  parts: [{ kind: "text", text }]
132
166
  }, options);
133
167
  }
168
+ /**
169
+ * Send a text message and wait for completion
170
+ * Returns the completed task with response artifacts
171
+ */
172
+ async sendTextAndWait(toAgentId, text, options) {
173
+ const task = await this.sendText(toAgentId, text, options);
174
+ return this.waitForTask(task.id, options);
175
+ }
176
+ /**
177
+ * Send a text message and wait for the text response
178
+ * This is a convenience method that extracts the response text automatically
179
+ */
180
+ async askText(toAgentId, text, options) {
181
+ const task = await this.sendTextAndWait(toAgentId, text, options);
182
+ if (task.status.state === "failed") {
183
+ throw new Error(task.status.message || "Task failed");
184
+ }
185
+ return getTaskResponseText(task);
186
+ }
187
+ /**
188
+ * Wait for a task to complete (polling)
189
+ */
190
+ async waitForTask(taskId, options) {
191
+ const pollInterval = options?.pollIntervalMs ?? 1e3;
192
+ const maxWait = options?.maxWaitMs ?? 3e5;
193
+ const startTime = Date.now();
194
+ while (Date.now() - startTime < maxWait) {
195
+ const task = await this.getTask(taskId);
196
+ if (task.status.state === "completed" || task.status.state === "failed" || task.status.state === "canceled" || task.status.state === "rejected") {
197
+ return task;
198
+ }
199
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
200
+ }
201
+ throw new Error(`Task ${taskId} did not complete within ${maxWait}ms`);
202
+ }
134
203
  /**
135
204
  * Get a task by ID
136
205
  */
@@ -190,25 +259,38 @@ var GopherHole = class extends EventEmitter {
190
259
  /**
191
260
  * Make a JSON-RPC call to the A2A endpoint
192
261
  */
193
- async rpc(method, params) {
194
- const response = await fetch(`${this.apiUrl}/a2a`, {
195
- method: "POST",
196
- headers: {
197
- "Content-Type": "application/json",
198
- "Authorization": `Bearer ${this.apiKey}`
199
- },
200
- body: JSON.stringify({
201
- jsonrpc: "2.0",
202
- method,
203
- params,
204
- id: Date.now()
205
- })
206
- });
207
- const data = await response.json();
208
- if (data.error) {
209
- throw new Error(data.error.message || "RPC error");
262
+ async rpc(method, params, timeoutMs) {
263
+ const timeout = timeoutMs ?? this.requestTimeout;
264
+ const controller = new AbortController();
265
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
266
+ try {
267
+ const response = await fetch(`${this.apiUrl}/a2a`, {
268
+ method: "POST",
269
+ headers: {
270
+ "Content-Type": "application/json",
271
+ "Authorization": `Bearer ${this.apiKey}`
272
+ },
273
+ body: JSON.stringify({
274
+ jsonrpc: "2.0",
275
+ method,
276
+ params,
277
+ id: Date.now()
278
+ }),
279
+ signal: controller.signal
280
+ });
281
+ const data = await response.json();
282
+ if (data.error) {
283
+ throw new Error(data.error.message || "RPC error");
284
+ }
285
+ return data.result;
286
+ } catch (err) {
287
+ if (err instanceof Error && err.name === "AbortError") {
288
+ throw new Error(`Request timeout after ${timeout}ms`);
289
+ }
290
+ throw err;
291
+ } finally {
292
+ clearTimeout(timeoutId);
210
293
  }
211
- return data.result;
212
294
  }
213
295
  /**
214
296
  * Handle incoming WebSocket messages
@@ -436,5 +518,6 @@ var index_default = GopherHole;
436
518
  export {
437
519
  GopherHole,
438
520
  JsonRpcErrorCodes,
439
- index_default as default
521
+ index_default as default,
522
+ getTaskResponseText
440
523
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gopherhole/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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/index.ts CHANGED
@@ -16,6 +16,8 @@ export interface GopherHoleOptions {
16
16
  reconnectDelay?: number;
17
17
  /** Max reconnect attempts */
18
18
  maxReconnectAttempts?: number;
19
+ /** Default request timeout in ms (default: 30000) */
20
+ requestTimeout?: number;
19
21
  }
20
22
 
21
23
  /** Agent card configuration for registration */
@@ -73,12 +75,55 @@ export interface TaskStatus {
73
75
  }
74
76
 
75
77
  export interface Artifact {
76
- name: string;
77
- mimeType: string;
78
+ name?: string;
79
+ artifactId?: string;
80
+ mimeType?: string;
81
+ parts?: MessagePart[];
78
82
  data?: string;
79
83
  uri?: string;
80
84
  }
81
85
 
86
+ /**
87
+ * Extract text response from a completed task.
88
+ * Checks artifacts first (where responses live), then falls back to history.
89
+ */
90
+ export function getTaskResponseText(task: Task): string {
91
+ // Check artifacts first (this is where responses from other agents appear)
92
+ if (task.artifacts?.length) {
93
+ const texts: string[] = [];
94
+ for (const artifact of task.artifacts) {
95
+ if (artifact.parts) {
96
+ for (const part of artifact.parts) {
97
+ if (part.kind === 'text' && part.text) {
98
+ texts.push(part.text);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ if (texts.length > 0) {
104
+ return texts.join('\n');
105
+ }
106
+ }
107
+
108
+ // Fall back to history (last message)
109
+ if (task.history?.length) {
110
+ const lastMessage = task.history[task.history.length - 1];
111
+ if (lastMessage.parts) {
112
+ const texts: string[] = [];
113
+ for (const part of lastMessage.parts) {
114
+ if (part.kind === 'text' && part.text) {
115
+ texts.push(part.text);
116
+ }
117
+ }
118
+ if (texts.length > 0) {
119
+ return texts.join('\n');
120
+ }
121
+ }
122
+ }
123
+
124
+ return '';
125
+ }
126
+
82
127
  export interface SendOptions {
83
128
  /** Existing context/conversation ID */
84
129
  contextId?: string;
@@ -86,6 +131,15 @@ export interface SendOptions {
86
131
  pushNotificationUrl?: string;
87
132
  /** History length to include */
88
133
  historyLength?: number;
134
+ /** Request timeout in ms (overrides default) */
135
+ timeoutMs?: number;
136
+ }
137
+
138
+ export interface SendAndWaitOptions extends SendOptions {
139
+ /** Polling interval in ms (default: 1000) */
140
+ pollIntervalMs?: number;
141
+ /** Max wait time in ms (default: 300000 = 5 min) */
142
+ maxWaitMs?: number;
89
143
  }
90
144
 
91
145
  type EventMap = {
@@ -107,6 +161,7 @@ export class GopherHole extends EventEmitter<EventMap> {
107
161
  private autoReconnect: boolean;
108
162
  private reconnectDelay: number;
109
163
  private maxReconnectAttempts: number;
164
+ private requestTimeout: number;
110
165
  private reconnectAttempts = 0;
111
166
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
112
167
  private pingInterval: ReturnType<typeof setInterval> | null = null;
@@ -127,6 +182,7 @@ export class GopherHole extends EventEmitter<EventMap> {
127
182
  this.autoReconnect = options.autoReconnect ?? true;
128
183
  this.reconnectDelay = options.reconnectDelay ?? 1000;
129
184
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
185
+ this.requestTimeout = options.requestTimeout ?? 30000;
130
186
  }
131
187
 
132
188
  /**
@@ -209,13 +265,14 @@ export class GopherHole extends EventEmitter<EventMap> {
209
265
  * Send a message to another agent
210
266
  */
211
267
  async send(toAgentId: string, payload: MessagePayload, options?: SendOptions): Promise<Task> {
268
+ const { timeoutMs, ...config } = options || {};
212
269
  const response = await this.rpc('message/send', {
213
270
  message: payload,
214
271
  configuration: {
215
272
  agentId: toAgentId,
216
- ...options,
273
+ ...config,
217
274
  },
218
- });
275
+ }, timeoutMs);
219
276
 
220
277
  return response as Task;
221
278
  }
@@ -230,6 +287,50 @@ export class GopherHole extends EventEmitter<EventMap> {
230
287
  }, options);
231
288
  }
232
289
 
290
+ /**
291
+ * Send a text message and wait for completion
292
+ * Returns the completed task with response artifacts
293
+ */
294
+ async sendTextAndWait(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<Task> {
295
+ const task = await this.sendText(toAgentId, text, options);
296
+ return this.waitForTask(task.id, options);
297
+ }
298
+
299
+ /**
300
+ * Send a text message and wait for the text response
301
+ * This is a convenience method that extracts the response text automatically
302
+ */
303
+ async askText(toAgentId: string, text: string, options?: SendAndWaitOptions): Promise<string> {
304
+ const task = await this.sendTextAndWait(toAgentId, text, options);
305
+ if (task.status.state === 'failed') {
306
+ throw new Error(task.status.message || 'Task failed');
307
+ }
308
+ return getTaskResponseText(task);
309
+ }
310
+
311
+ /**
312
+ * Wait for a task to complete (polling)
313
+ */
314
+ async waitForTask(taskId: string, options?: SendAndWaitOptions): Promise<Task> {
315
+ const pollInterval = options?.pollIntervalMs ?? 1000;
316
+ const maxWait = options?.maxWaitMs ?? 300000; // 5 min default
317
+ const startTime = Date.now();
318
+
319
+ while (Date.now() - startTime < maxWait) {
320
+ const task = await this.getTask(taskId);
321
+
322
+ if (task.status.state === 'completed' || task.status.state === 'failed' ||
323
+ task.status.state === 'canceled' || task.status.state === 'rejected') {
324
+ return task;
325
+ }
326
+
327
+ // Wait before polling again
328
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
329
+ }
330
+
331
+ throw new Error(`Task ${taskId} did not complete within ${maxWait}ms`);
332
+ }
333
+
233
334
  /**
234
335
  * Get a task by ID
235
336
  */
@@ -305,28 +406,42 @@ export class GopherHole extends EventEmitter<EventMap> {
305
406
  /**
306
407
  * Make a JSON-RPC call to the A2A endpoint
307
408
  */
308
- private async rpc(method: string, params: Record<string, unknown>): Promise<unknown> {
309
- const response = await fetch(`${this.apiUrl}/a2a`, {
310
- method: 'POST',
311
- headers: {
312
- 'Content-Type': 'application/json',
313
- 'Authorization': `Bearer ${this.apiKey}`,
314
- },
315
- body: JSON.stringify({
316
- jsonrpc: '2.0',
317
- method,
318
- params,
319
- id: Date.now(),
320
- }),
321
- });
409
+ private async rpc(method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown> {
410
+ const timeout = timeoutMs ?? this.requestTimeout;
411
+ const controller = new AbortController();
412
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
322
413
 
323
- const data = await response.json();
414
+ try {
415
+ const response = await fetch(`${this.apiUrl}/a2a`, {
416
+ method: 'POST',
417
+ headers: {
418
+ 'Content-Type': 'application/json',
419
+ 'Authorization': `Bearer ${this.apiKey}`,
420
+ },
421
+ body: JSON.stringify({
422
+ jsonrpc: '2.0',
423
+ method,
424
+ params,
425
+ id: Date.now(),
426
+ }),
427
+ signal: controller.signal,
428
+ });
324
429
 
325
- if (data.error) {
326
- throw new Error(data.error.message || 'RPC error');
327
- }
430
+ const data = await response.json();
431
+
432
+ if (data.error) {
433
+ throw new Error(data.error.message || 'RPC error');
434
+ }
328
435
 
329
- return data.result;
436
+ return data.result;
437
+ } catch (err) {
438
+ if (err instanceof Error && err.name === 'AbortError') {
439
+ throw new Error(`Request timeout after ${timeout}ms`);
440
+ }
441
+ throw err;
442
+ } finally {
443
+ clearTimeout(timeoutId);
444
+ }
330
445
  }
331
446
 
332
447
  /**