@renseiai/agentfactory 0.8.0

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.
Files changed (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/src/config/index.d.ts +3 -0
  4. package/dist/src/config/index.d.ts.map +1 -0
  5. package/dist/src/config/index.js +1 -0
  6. package/dist/src/config/repository-config.d.ts +44 -0
  7. package/dist/src/config/repository-config.d.ts.map +1 -0
  8. package/dist/src/config/repository-config.js +88 -0
  9. package/dist/src/config/repository-config.test.d.ts +2 -0
  10. package/dist/src/config/repository-config.test.d.ts.map +1 -0
  11. package/dist/src/config/repository-config.test.js +249 -0
  12. package/dist/src/deployment/deployment-checker.d.ts +110 -0
  13. package/dist/src/deployment/deployment-checker.d.ts.map +1 -0
  14. package/dist/src/deployment/deployment-checker.js +242 -0
  15. package/dist/src/deployment/index.d.ts +3 -0
  16. package/dist/src/deployment/index.d.ts.map +1 -0
  17. package/dist/src/deployment/index.js +2 -0
  18. package/dist/src/frontend/index.d.ts +2 -0
  19. package/dist/src/frontend/index.d.ts.map +1 -0
  20. package/dist/src/frontend/index.js +1 -0
  21. package/dist/src/frontend/types.d.ts +106 -0
  22. package/dist/src/frontend/types.d.ts.map +1 -0
  23. package/dist/src/frontend/types.js +11 -0
  24. package/dist/src/governor/decision-engine.d.ts +52 -0
  25. package/dist/src/governor/decision-engine.d.ts.map +1 -0
  26. package/dist/src/governor/decision-engine.js +220 -0
  27. package/dist/src/governor/decision-engine.test.d.ts +2 -0
  28. package/dist/src/governor/decision-engine.test.d.ts.map +1 -0
  29. package/dist/src/governor/decision-engine.test.js +629 -0
  30. package/dist/src/governor/event-bus.d.ts +43 -0
  31. package/dist/src/governor/event-bus.d.ts.map +1 -0
  32. package/dist/src/governor/event-bus.js +8 -0
  33. package/dist/src/governor/event-deduplicator.d.ts +43 -0
  34. package/dist/src/governor/event-deduplicator.d.ts.map +1 -0
  35. package/dist/src/governor/event-deduplicator.js +53 -0
  36. package/dist/src/governor/event-driven-governor.d.ts +131 -0
  37. package/dist/src/governor/event-driven-governor.d.ts.map +1 -0
  38. package/dist/src/governor/event-driven-governor.js +379 -0
  39. package/dist/src/governor/event-driven-governor.test.d.ts +2 -0
  40. package/dist/src/governor/event-driven-governor.test.d.ts.map +1 -0
  41. package/dist/src/governor/event-driven-governor.test.js +673 -0
  42. package/dist/src/governor/event-types.d.ts +78 -0
  43. package/dist/src/governor/event-types.d.ts.map +1 -0
  44. package/dist/src/governor/event-types.js +32 -0
  45. package/dist/src/governor/governor-types.d.ts +82 -0
  46. package/dist/src/governor/governor-types.d.ts.map +1 -0
  47. package/dist/src/governor/governor-types.js +21 -0
  48. package/dist/src/governor/governor.d.ts +100 -0
  49. package/dist/src/governor/governor.d.ts.map +1 -0
  50. package/dist/src/governor/governor.js +262 -0
  51. package/dist/src/governor/governor.test.d.ts +2 -0
  52. package/dist/src/governor/governor.test.d.ts.map +1 -0
  53. package/dist/src/governor/governor.test.js +514 -0
  54. package/dist/src/governor/human-touchpoints.d.ts +131 -0
  55. package/dist/src/governor/human-touchpoints.d.ts.map +1 -0
  56. package/dist/src/governor/human-touchpoints.js +251 -0
  57. package/dist/src/governor/human-touchpoints.test.d.ts +2 -0
  58. package/dist/src/governor/human-touchpoints.test.d.ts.map +1 -0
  59. package/dist/src/governor/human-touchpoints.test.js +366 -0
  60. package/dist/src/governor/in-memory-event-bus.d.ts +29 -0
  61. package/dist/src/governor/in-memory-event-bus.d.ts.map +1 -0
  62. package/dist/src/governor/in-memory-event-bus.js +79 -0
  63. package/dist/src/governor/index.d.ts +14 -0
  64. package/dist/src/governor/index.d.ts.map +1 -0
  65. package/dist/src/governor/index.js +13 -0
  66. package/dist/src/governor/override-parser.d.ts +60 -0
  67. package/dist/src/governor/override-parser.d.ts.map +1 -0
  68. package/dist/src/governor/override-parser.js +98 -0
  69. package/dist/src/governor/override-parser.test.d.ts +2 -0
  70. package/dist/src/governor/override-parser.test.d.ts.map +1 -0
  71. package/dist/src/governor/override-parser.test.js +312 -0
  72. package/dist/src/governor/platform-adapter.d.ts +69 -0
  73. package/dist/src/governor/platform-adapter.d.ts.map +1 -0
  74. package/dist/src/governor/platform-adapter.js +11 -0
  75. package/dist/src/governor/processing-state.d.ts +66 -0
  76. package/dist/src/governor/processing-state.d.ts.map +1 -0
  77. package/dist/src/governor/processing-state.js +43 -0
  78. package/dist/src/governor/processing-state.test.d.ts +2 -0
  79. package/dist/src/governor/processing-state.test.d.ts.map +1 -0
  80. package/dist/src/governor/processing-state.test.js +96 -0
  81. package/dist/src/governor/top-of-funnel.d.ts +118 -0
  82. package/dist/src/governor/top-of-funnel.d.ts.map +1 -0
  83. package/dist/src/governor/top-of-funnel.js +168 -0
  84. package/dist/src/governor/top-of-funnel.test.d.ts +2 -0
  85. package/dist/src/governor/top-of-funnel.test.d.ts.map +1 -0
  86. package/dist/src/governor/top-of-funnel.test.js +331 -0
  87. package/dist/src/index.d.ts +11 -0
  88. package/dist/src/index.d.ts.map +1 -0
  89. package/dist/src/index.js +10 -0
  90. package/dist/src/linear-cli.d.ts +38 -0
  91. package/dist/src/linear-cli.d.ts.map +1 -0
  92. package/dist/src/linear-cli.js +674 -0
  93. package/dist/src/logger.d.ts +117 -0
  94. package/dist/src/logger.d.ts.map +1 -0
  95. package/dist/src/logger.js +430 -0
  96. package/dist/src/manifest/generate.d.ts +20 -0
  97. package/dist/src/manifest/generate.d.ts.map +1 -0
  98. package/dist/src/manifest/generate.js +65 -0
  99. package/dist/src/manifest/index.d.ts +4 -0
  100. package/dist/src/manifest/index.d.ts.map +1 -0
  101. package/dist/src/manifest/index.js +2 -0
  102. package/dist/src/manifest/route-manifest.d.ts +34 -0
  103. package/dist/src/manifest/route-manifest.d.ts.map +1 -0
  104. package/dist/src/manifest/route-manifest.js +148 -0
  105. package/dist/src/orchestrator/activity-emitter.d.ts +119 -0
  106. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -0
  107. package/dist/src/orchestrator/activity-emitter.js +306 -0
  108. package/dist/src/orchestrator/api-activity-emitter.d.ts +167 -0
  109. package/dist/src/orchestrator/api-activity-emitter.d.ts.map +1 -0
  110. package/dist/src/orchestrator/api-activity-emitter.js +417 -0
  111. package/dist/src/orchestrator/heartbeat-writer.d.ts +57 -0
  112. package/dist/src/orchestrator/heartbeat-writer.d.ts.map +1 -0
  113. package/dist/src/orchestrator/heartbeat-writer.js +137 -0
  114. package/dist/src/orchestrator/index.d.ts +20 -0
  115. package/dist/src/orchestrator/index.d.ts.map +1 -0
  116. package/dist/src/orchestrator/index.js +22 -0
  117. package/dist/src/orchestrator/log-analyzer.d.ts +160 -0
  118. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -0
  119. package/dist/src/orchestrator/log-analyzer.js +572 -0
  120. package/dist/src/orchestrator/log-config.d.ts +39 -0
  121. package/dist/src/orchestrator/log-config.d.ts.map +1 -0
  122. package/dist/src/orchestrator/log-config.js +45 -0
  123. package/dist/src/orchestrator/orchestrator.d.ts +316 -0
  124. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -0
  125. package/dist/src/orchestrator/orchestrator.js +3290 -0
  126. package/dist/src/orchestrator/parse-work-result.d.ts +16 -0
  127. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -0
  128. package/dist/src/orchestrator/parse-work-result.js +135 -0
  129. package/dist/src/orchestrator/parse-work-result.test.d.ts +2 -0
  130. package/dist/src/orchestrator/parse-work-result.test.d.ts.map +1 -0
  131. package/dist/src/orchestrator/parse-work-result.test.js +234 -0
  132. package/dist/src/orchestrator/progress-logger.d.ts +72 -0
  133. package/dist/src/orchestrator/progress-logger.d.ts.map +1 -0
  134. package/dist/src/orchestrator/progress-logger.js +135 -0
  135. package/dist/src/orchestrator/session-logger.d.ts +159 -0
  136. package/dist/src/orchestrator/session-logger.d.ts.map +1 -0
  137. package/dist/src/orchestrator/session-logger.js +275 -0
  138. package/dist/src/orchestrator/state-recovery.d.ts +96 -0
  139. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -0
  140. package/dist/src/orchestrator/state-recovery.js +302 -0
  141. package/dist/src/orchestrator/state-types.d.ts +165 -0
  142. package/dist/src/orchestrator/state-types.d.ts.map +1 -0
  143. package/dist/src/orchestrator/state-types.js +7 -0
  144. package/dist/src/orchestrator/stream-parser.d.ts +151 -0
  145. package/dist/src/orchestrator/stream-parser.d.ts.map +1 -0
  146. package/dist/src/orchestrator/stream-parser.js +137 -0
  147. package/dist/src/orchestrator/types.d.ts +232 -0
  148. package/dist/src/orchestrator/types.d.ts.map +1 -0
  149. package/dist/src/orchestrator/types.js +4 -0
  150. package/dist/src/orchestrator/validate-git-remote.test.d.ts +2 -0
  151. package/dist/src/orchestrator/validate-git-remote.test.d.ts.map +1 -0
  152. package/dist/src/orchestrator/validate-git-remote.test.js +61 -0
  153. package/dist/src/providers/a2a-auth.d.ts +81 -0
  154. package/dist/src/providers/a2a-auth.d.ts.map +1 -0
  155. package/dist/src/providers/a2a-auth.js +188 -0
  156. package/dist/src/providers/a2a-auth.test.d.ts +2 -0
  157. package/dist/src/providers/a2a-auth.test.d.ts.map +1 -0
  158. package/dist/src/providers/a2a-auth.test.js +232 -0
  159. package/dist/src/providers/a2a-provider.d.ts +254 -0
  160. package/dist/src/providers/a2a-provider.d.ts.map +1 -0
  161. package/dist/src/providers/a2a-provider.integration.test.d.ts +9 -0
  162. package/dist/src/providers/a2a-provider.integration.test.d.ts.map +1 -0
  163. package/dist/src/providers/a2a-provider.integration.test.js +665 -0
  164. package/dist/src/providers/a2a-provider.js +811 -0
  165. package/dist/src/providers/a2a-provider.test.d.ts +2 -0
  166. package/dist/src/providers/a2a-provider.test.d.ts.map +1 -0
  167. package/dist/src/providers/a2a-provider.test.js +681 -0
  168. package/dist/src/providers/amp-provider.d.ts +20 -0
  169. package/dist/src/providers/amp-provider.d.ts.map +1 -0
  170. package/dist/src/providers/amp-provider.js +24 -0
  171. package/dist/src/providers/claude-provider.d.ts +18 -0
  172. package/dist/src/providers/claude-provider.d.ts.map +1 -0
  173. package/dist/src/providers/claude-provider.js +437 -0
  174. package/dist/src/providers/codex-provider.d.ts +133 -0
  175. package/dist/src/providers/codex-provider.d.ts.map +1 -0
  176. package/dist/src/providers/codex-provider.js +381 -0
  177. package/dist/src/providers/codex-provider.test.d.ts +2 -0
  178. package/dist/src/providers/codex-provider.test.d.ts.map +1 -0
  179. package/dist/src/providers/codex-provider.test.js +387 -0
  180. package/dist/src/providers/index.d.ts +44 -0
  181. package/dist/src/providers/index.d.ts.map +1 -0
  182. package/dist/src/providers/index.js +85 -0
  183. package/dist/src/providers/spring-ai-provider.d.ts +90 -0
  184. package/dist/src/providers/spring-ai-provider.d.ts.map +1 -0
  185. package/dist/src/providers/spring-ai-provider.integration.test.d.ts +13 -0
  186. package/dist/src/providers/spring-ai-provider.integration.test.d.ts.map +1 -0
  187. package/dist/src/providers/spring-ai-provider.integration.test.js +351 -0
  188. package/dist/src/providers/spring-ai-provider.js +317 -0
  189. package/dist/src/providers/spring-ai-provider.test.d.ts +2 -0
  190. package/dist/src/providers/spring-ai-provider.test.d.ts.map +1 -0
  191. package/dist/src/providers/spring-ai-provider.test.js +200 -0
  192. package/dist/src/providers/types.d.ts +165 -0
  193. package/dist/src/providers/types.d.ts.map +1 -0
  194. package/dist/src/providers/types.js +13 -0
  195. package/dist/src/templates/adapters.d.ts +51 -0
  196. package/dist/src/templates/adapters.d.ts.map +1 -0
  197. package/dist/src/templates/adapters.js +104 -0
  198. package/dist/src/templates/adapters.test.d.ts +2 -0
  199. package/dist/src/templates/adapters.test.d.ts.map +1 -0
  200. package/dist/src/templates/adapters.test.js +165 -0
  201. package/dist/src/templates/agent-definition.d.ts +85 -0
  202. package/dist/src/templates/agent-definition.d.ts.map +1 -0
  203. package/dist/src/templates/agent-definition.js +97 -0
  204. package/dist/src/templates/agent-definition.test.d.ts +2 -0
  205. package/dist/src/templates/agent-definition.test.d.ts.map +1 -0
  206. package/dist/src/templates/agent-definition.test.js +209 -0
  207. package/dist/src/templates/index.d.ts +14 -0
  208. package/dist/src/templates/index.d.ts.map +1 -0
  209. package/dist/src/templates/index.js +11 -0
  210. package/dist/src/templates/loader.d.ts +41 -0
  211. package/dist/src/templates/loader.d.ts.map +1 -0
  212. package/dist/src/templates/loader.js +114 -0
  213. package/dist/src/templates/registry.d.ts +80 -0
  214. package/dist/src/templates/registry.d.ts.map +1 -0
  215. package/dist/src/templates/registry.js +177 -0
  216. package/dist/src/templates/registry.test.d.ts +2 -0
  217. package/dist/src/templates/registry.test.d.ts.map +1 -0
  218. package/dist/src/templates/registry.test.js +198 -0
  219. package/dist/src/templates/renderer.d.ts +29 -0
  220. package/dist/src/templates/renderer.d.ts.map +1 -0
  221. package/dist/src/templates/renderer.js +35 -0
  222. package/dist/src/templates/strategy-templates.test.d.ts +2 -0
  223. package/dist/src/templates/strategy-templates.test.d.ts.map +1 -0
  224. package/dist/src/templates/strategy-templates.test.js +619 -0
  225. package/dist/src/templates/types.d.ts +233 -0
  226. package/dist/src/templates/types.d.ts.map +1 -0
  227. package/dist/src/templates/types.js +127 -0
  228. package/dist/src/templates/types.test.d.ts +2 -0
  229. package/dist/src/templates/types.test.d.ts.map +1 -0
  230. package/dist/src/templates/types.test.js +232 -0
  231. package/dist/src/tools/index.d.ts +6 -0
  232. package/dist/src/tools/index.d.ts.map +1 -0
  233. package/dist/src/tools/index.js +3 -0
  234. package/dist/src/tools/linear-runner.d.ts +34 -0
  235. package/dist/src/tools/linear-runner.d.ts.map +1 -0
  236. package/dist/src/tools/linear-runner.js +700 -0
  237. package/dist/src/tools/plugins/linear.d.ts +9 -0
  238. package/dist/src/tools/plugins/linear.d.ts.map +1 -0
  239. package/dist/src/tools/plugins/linear.js +138 -0
  240. package/dist/src/tools/registry.d.ts +9 -0
  241. package/dist/src/tools/registry.d.ts.map +1 -0
  242. package/dist/src/tools/registry.js +18 -0
  243. package/dist/src/tools/types.d.ts +18 -0
  244. package/dist/src/tools/types.d.ts.map +1 -0
  245. package/dist/src/tools/types.js +1 -0
  246. package/package.json +78 -0
@@ -0,0 +1,811 @@
1
+ /**
2
+ * A2A (Agent-to-Agent) Client Provider
3
+ *
4
+ * Invokes external A2A-compliant agents over HTTP using the Google A2A protocol.
5
+ * Uses JSON-RPC 2.0 for requests and SSE streaming for responses.
6
+ *
7
+ * A2A Protocol overview:
8
+ * - Agent discovery via GET /.well-known/agent-card.json
9
+ * - Task creation via POST /a2a with JSON-RPC method "message/send" or "message/stream"
10
+ * - Task lifecycle: submitted → working → completed/failed/canceled
11
+ * - Input-required is a paused state requesting user input
12
+ * - SSE events: TaskStatusUpdateEvent, TaskArtifactUpdateEvent
13
+ *
14
+ * Env vars:
15
+ * A2A_AGENT_URL — base URL of the A2A agent (e.g., https://agent.example.com)
16
+ * A2A_AGENT_URL_{WORKTYPE} — work-type-specific override (e.g., A2A_AGENT_URL_RESEARCH)
17
+ * A2A_API_KEY — API key for authentication (sent as x-api-key header)
18
+ * A2A_BEARER_TOKEN — Bearer token for authentication (sent as Authorization header)
19
+ */
20
+ import { randomUUID } from 'crypto';
21
+ // ---------------------------------------------------------------------------
22
+ // Agent Card Discovery
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Fetch and parse the A2A Agent Card from the well-known endpoint.
26
+ *
27
+ * @param baseUrl - Base URL of the A2A agent (e.g., "https://agent.example.com")
28
+ * @returns Parsed agent card
29
+ * @throws Error if the fetch fails or the response is not valid JSON
30
+ */
31
+ export async function fetchAgentCard(baseUrl) {
32
+ const url = `${baseUrl.replace(/\/+$/, '')}/.well-known/agent-card.json`;
33
+ const response = await fetch(url, {
34
+ method: 'GET',
35
+ headers: { 'Accept': 'application/json' },
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to fetch A2A agent card from ${url}: ${response.status} ${response.statusText}`);
39
+ }
40
+ const card = (await response.json());
41
+ if (!card.name || !card.url) {
42
+ throw new Error(`Invalid A2A agent card from ${url}: missing required fields "name" and/or "url"`);
43
+ }
44
+ return card;
45
+ }
46
+ /**
47
+ * Extract text content from an array of A2A parts.
48
+ * Concatenates all text parts, includes data parts as JSON, and notes file parts.
49
+ */
50
+ function extractTextFromParts(parts) {
51
+ const texts = [];
52
+ for (const part of parts) {
53
+ switch (part.type) {
54
+ case 'text':
55
+ texts.push(part.text);
56
+ break;
57
+ case 'data':
58
+ texts.push(JSON.stringify(part.data));
59
+ break;
60
+ case 'file':
61
+ texts.push(`[File: ${part.file.name ?? part.file.uri ?? 'unnamed'}]`);
62
+ break;
63
+ }
64
+ }
65
+ return texts.join('\n');
66
+ }
67
+ /**
68
+ * Map a single A2A task event to one or more normalized AgentEvents.
69
+ * Exported for unit testing — the AgentHandle uses this internally.
70
+ *
71
+ * @param event - The A2A SSE task event
72
+ * @param state - Mutable state accumulator for the session
73
+ * @returns Array of normalized AgentEvent objects
74
+ */
75
+ export function mapA2aTaskEvent(event, state) {
76
+ switch (event.type) {
77
+ case 'TaskStatusUpdate': {
78
+ // Track task and session IDs
79
+ if (event.id && !state.taskId) {
80
+ state.taskId = event.id;
81
+ }
82
+ if (event.sessionId && !state.sessionId) {
83
+ state.sessionId = event.sessionId;
84
+ }
85
+ const status = event.status;
86
+ switch (status.state) {
87
+ case 'submitted': {
88
+ // Task has been created — emit init event
89
+ const sessionId = event.sessionId ?? event.id;
90
+ state.sessionId = sessionId;
91
+ state.taskId = event.id;
92
+ return [{
93
+ type: 'init',
94
+ sessionId,
95
+ raw: event,
96
+ }];
97
+ }
98
+ case 'working': {
99
+ state.turnCount++;
100
+ const events = [];
101
+ // If there is a message with the status, emit the text
102
+ if (status.message && status.message.parts.length > 0) {
103
+ const text = extractTextFromParts(status.message.parts);
104
+ if (text) {
105
+ events.push({
106
+ type: 'assistant_text',
107
+ text,
108
+ raw: event,
109
+ });
110
+ }
111
+ }
112
+ // If no message content, emit a system event noting work is in progress
113
+ if (events.length === 0) {
114
+ events.push({
115
+ type: 'system',
116
+ subtype: 'working',
117
+ message: `Task ${event.id} is working (turn ${state.turnCount})`,
118
+ raw: event,
119
+ });
120
+ }
121
+ return events;
122
+ }
123
+ case 'input-required': {
124
+ const message = status.message
125
+ ? extractTextFromParts(status.message.parts)
126
+ : 'Agent requires additional input';
127
+ return [{
128
+ type: 'system',
129
+ subtype: 'input_required',
130
+ message,
131
+ raw: event,
132
+ }];
133
+ }
134
+ case 'completed': {
135
+ const message = status.message
136
+ ? extractTextFromParts(status.message.parts)
137
+ : undefined;
138
+ return [{
139
+ type: 'result',
140
+ success: true,
141
+ message,
142
+ cost: {
143
+ inputTokens: state.totalInputTokens || undefined,
144
+ outputTokens: state.totalOutputTokens || undefined,
145
+ numTurns: state.turnCount || undefined,
146
+ },
147
+ raw: event,
148
+ }];
149
+ }
150
+ case 'failed': {
151
+ const errorMessage = status.message
152
+ ? extractTextFromParts(status.message.parts)
153
+ : 'Task failed';
154
+ return [{
155
+ type: 'result',
156
+ success: false,
157
+ errors: [errorMessage],
158
+ errorSubtype: 'task_failed',
159
+ raw: event,
160
+ }];
161
+ }
162
+ case 'canceled': {
163
+ const cancelMessage = status.message
164
+ ? extractTextFromParts(status.message.parts)
165
+ : 'Task canceled';
166
+ return [{
167
+ type: 'result',
168
+ success: false,
169
+ errors: [cancelMessage],
170
+ errorSubtype: 'canceled',
171
+ raw: event,
172
+ }];
173
+ }
174
+ default:
175
+ return [{
176
+ type: 'system',
177
+ subtype: 'unknown',
178
+ message: `Unknown A2A task status: ${status.state}`,
179
+ raw: event,
180
+ }];
181
+ }
182
+ }
183
+ case 'TaskArtifactUpdate': {
184
+ // Track task and session IDs
185
+ if (event.id && !state.taskId) {
186
+ state.taskId = event.id;
187
+ }
188
+ if (event.sessionId && !state.sessionId) {
189
+ state.sessionId = event.sessionId;
190
+ }
191
+ const artifact = event.artifact;
192
+ const text = extractTextFromParts(artifact.parts);
193
+ return [{
194
+ type: 'assistant_text',
195
+ text: text || `[Artifact: ${artifact.name ?? 'unnamed'}]`,
196
+ raw: event,
197
+ }];
198
+ }
199
+ default:
200
+ return [{
201
+ type: 'system',
202
+ subtype: 'unknown',
203
+ message: `Unhandled A2A event type: ${event.type}`,
204
+ raw: event,
205
+ }];
206
+ }
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // Provider
210
+ // ---------------------------------------------------------------------------
211
+ export class A2aProvider {
212
+ name = 'a2a';
213
+ spawn(config) {
214
+ return this.createHandle(config);
215
+ }
216
+ resume(sessionId, config) {
217
+ return this.createHandle(config, sessionId);
218
+ }
219
+ createHandle(config, resumeSessionId) {
220
+ const abortController = config.abortController;
221
+ // Resolve the A2A agent URL — check work-type-specific env vars first
222
+ const agentUrl = resolveAgentUrl(config.env);
223
+ if (!agentUrl) {
224
+ return new A2aAgentHandle({
225
+ agentUrl: '',
226
+ abortController,
227
+ initError: 'A2A_AGENT_URL environment variable is not set. ' +
228
+ 'Set A2A_AGENT_URL or A2A_AGENT_URL_{WORKTYPE} to the base URL of the A2A agent.',
229
+ });
230
+ }
231
+ // Resolve authentication headers
232
+ const authHeaders = resolveAuthHeaders(config.env);
233
+ // Build the initial JSON-RPC params
234
+ const messageParams = {
235
+ message: {
236
+ role: 'user',
237
+ parts: [{ type: 'text', text: config.prompt }],
238
+ },
239
+ };
240
+ // If resuming, include the existing session/task ID
241
+ if (resumeSessionId) {
242
+ messageParams.sessionId = resumeSessionId;
243
+ }
244
+ return new A2aAgentHandle({
245
+ agentUrl,
246
+ abortController,
247
+ authHeaders,
248
+ initialParams: messageParams,
249
+ resumeSessionId: resumeSessionId ?? null,
250
+ });
251
+ }
252
+ }
253
+ class A2aAgentHandle {
254
+ sessionId = null;
255
+ agentUrl;
256
+ abortController;
257
+ authHeaders;
258
+ initialParams;
259
+ initError;
260
+ mapperState = {
261
+ sessionId: null,
262
+ taskId: null,
263
+ totalInputTokens: 0,
264
+ totalOutputTokens: 0,
265
+ turnCount: 0,
266
+ };
267
+ constructor(options) {
268
+ this.agentUrl = options.agentUrl;
269
+ this.abortController = options.abortController;
270
+ this.authHeaders = options.authHeaders ?? {};
271
+ this.initialParams = options.initialParams;
272
+ this.initError = options.initError;
273
+ if (options.resumeSessionId) {
274
+ this.sessionId = options.resumeSessionId;
275
+ this.mapperState.sessionId = options.resumeSessionId;
276
+ }
277
+ }
278
+ get stream() {
279
+ return this.createEventStream();
280
+ }
281
+ async injectMessage(text) {
282
+ const params = {
283
+ message: {
284
+ role: 'user',
285
+ parts: [{ type: 'text', text }],
286
+ },
287
+ };
288
+ // Include session/task IDs if we have them
289
+ if (this.mapperState.sessionId) {
290
+ params.sessionId = this.mapperState.sessionId;
291
+ }
292
+ if (this.mapperState.taskId) {
293
+ params.taskId = this.mapperState.taskId;
294
+ }
295
+ await this.sendJsonRpc('message/send', params);
296
+ }
297
+ async stop() {
298
+ // Send cancel request if we have a task ID
299
+ if (this.mapperState.taskId) {
300
+ try {
301
+ await this.sendJsonRpc('tasks/cancel', {
302
+ id: this.mapperState.taskId,
303
+ });
304
+ }
305
+ catch {
306
+ // Best-effort cancellation — ignore errors
307
+ }
308
+ }
309
+ this.abortController.abort();
310
+ }
311
+ /**
312
+ * Send a JSON-RPC 2.0 request to the A2A agent endpoint.
313
+ */
314
+ async sendJsonRpc(method, params) {
315
+ const rpcUrl = `${this.agentUrl.replace(/\/+$/, '')}/a2a`;
316
+ const request = {
317
+ jsonrpc: '2.0',
318
+ id: randomUUID(),
319
+ method,
320
+ params,
321
+ };
322
+ const response = await fetch(rpcUrl, {
323
+ method: 'POST',
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ 'Accept': 'text/event-stream, application/json',
327
+ ...this.authHeaders,
328
+ },
329
+ body: JSON.stringify(request),
330
+ signal: this.abortController.signal,
331
+ });
332
+ if (!response.ok) {
333
+ const body = await response.text().catch(() => '');
334
+ throw new Error(`A2A JSON-RPC request failed: ${response.status} ${response.statusText}` +
335
+ (body ? ` — ${body}` : ''));
336
+ }
337
+ return response;
338
+ }
339
+ /**
340
+ * Create the async event stream by sending the initial request and parsing
341
+ * either SSE (for message/stream) or JSON (for message/send) response.
342
+ */
343
+ async *createEventStream() {
344
+ // Handle init-time errors (e.g., missing agent URL)
345
+ if (this.initError) {
346
+ yield {
347
+ type: 'error',
348
+ message: this.initError,
349
+ raw: null,
350
+ };
351
+ yield {
352
+ type: 'result',
353
+ success: false,
354
+ errors: [this.initError],
355
+ errorSubtype: 'configuration_error',
356
+ raw: null,
357
+ };
358
+ return;
359
+ }
360
+ if (!this.initialParams) {
361
+ yield {
362
+ type: 'error',
363
+ message: 'No initial message params configured',
364
+ raw: null,
365
+ };
366
+ return;
367
+ }
368
+ let response;
369
+ let useStreaming = true;
370
+ try {
371
+ // Try streaming first (message/stream)
372
+ response = await this.sendJsonRpc('message/stream', this.initialParams);
373
+ }
374
+ catch (err) {
375
+ if (this.abortController.signal.aborted) {
376
+ yield {
377
+ type: 'result',
378
+ success: false,
379
+ errors: ['Request aborted'],
380
+ errorSubtype: 'aborted',
381
+ raw: null,
382
+ };
383
+ return;
384
+ }
385
+ // Fall back to non-streaming (message/send)
386
+ try {
387
+ response = await this.sendJsonRpc('message/send', this.initialParams);
388
+ useStreaming = false;
389
+ }
390
+ catch (sendErr) {
391
+ if (this.abortController.signal.aborted) {
392
+ yield {
393
+ type: 'result',
394
+ success: false,
395
+ errors: ['Request aborted'],
396
+ errorSubtype: 'aborted',
397
+ raw: null,
398
+ };
399
+ return;
400
+ }
401
+ const errorMessage = sendErr instanceof Error
402
+ ? sendErr.message
403
+ : 'Failed to connect to A2A agent';
404
+ yield {
405
+ type: 'error',
406
+ message: errorMessage,
407
+ raw: sendErr,
408
+ };
409
+ yield {
410
+ type: 'result',
411
+ success: false,
412
+ errors: [errorMessage],
413
+ errorSubtype: 'connection_error',
414
+ raw: null,
415
+ };
416
+ return;
417
+ }
418
+ }
419
+ const contentType = response.headers.get('content-type') ?? '';
420
+ if (useStreaming && contentType.includes('text/event-stream')) {
421
+ // Parse SSE stream
422
+ yield* this.parseSSEStream(response);
423
+ }
424
+ else {
425
+ // Parse single JSON response
426
+ yield* this.parseSingleResponse(response);
427
+ }
428
+ }
429
+ /**
430
+ * Parse an SSE event stream from the response body.
431
+ * Each SSE event has an optional `event:` field and a `data:` field with JSON.
432
+ */
433
+ async *parseSSEStream(response) {
434
+ const body = response.body;
435
+ if (!body) {
436
+ yield {
437
+ type: 'error',
438
+ message: 'A2A response has no body',
439
+ raw: null,
440
+ };
441
+ return;
442
+ }
443
+ const reader = body.getReader();
444
+ const decoder = new TextDecoder();
445
+ let buffer = '';
446
+ let currentEventType = '';
447
+ let currentData = '';
448
+ let hasResult = false;
449
+ try {
450
+ while (true) {
451
+ const { done, value } = await reader.read();
452
+ if (done)
453
+ break;
454
+ buffer += decoder.decode(value, { stream: true });
455
+ // Process complete lines
456
+ const lines = buffer.split('\n');
457
+ // Keep the last potentially incomplete line in the buffer
458
+ buffer = lines.pop() ?? '';
459
+ for (const line of lines) {
460
+ if (line.startsWith('event:')) {
461
+ currentEventType = line.slice(6).trim();
462
+ }
463
+ else if (line.startsWith('data:')) {
464
+ currentData = line.slice(5).trim();
465
+ }
466
+ else if (line === '' && currentData) {
467
+ // Empty line signals end of SSE event
468
+ let taskEvent;
469
+ try {
470
+ const parsed = JSON.parse(currentData);
471
+ // Use the event type from the SSE `event:` field if present,
472
+ // otherwise use the `type` field from the JSON data
473
+ if (currentEventType) {
474
+ parsed.type = currentEventType;
475
+ }
476
+ taskEvent = parsed;
477
+ }
478
+ catch {
479
+ // Non-JSON SSE data — emit as system event
480
+ yield {
481
+ type: 'system',
482
+ subtype: 'raw_output',
483
+ message: currentData,
484
+ raw: currentData,
485
+ };
486
+ currentEventType = '';
487
+ currentData = '';
488
+ continue;
489
+ }
490
+ const mapped = mapA2aTaskEvent(taskEvent, this.mapperState);
491
+ for (const agentEvent of mapped) {
492
+ if (agentEvent.type === 'init') {
493
+ this.sessionId = this.mapperState.sessionId;
494
+ }
495
+ if (agentEvent.type === 'result') {
496
+ hasResult = true;
497
+ }
498
+ yield agentEvent;
499
+ }
500
+ currentEventType = '';
501
+ currentData = '';
502
+ }
503
+ }
504
+ }
505
+ // Process any remaining data in the buffer
506
+ if (currentData) {
507
+ try {
508
+ const parsed = JSON.parse(currentData);
509
+ if (currentEventType) {
510
+ parsed.type = currentEventType;
511
+ }
512
+ const taskEvent = parsed;
513
+ const mapped = mapA2aTaskEvent(taskEvent, this.mapperState);
514
+ for (const agentEvent of mapped) {
515
+ if (agentEvent.type === 'init') {
516
+ this.sessionId = this.mapperState.sessionId;
517
+ }
518
+ if (agentEvent.type === 'result') {
519
+ hasResult = true;
520
+ }
521
+ yield agentEvent;
522
+ }
523
+ }
524
+ catch {
525
+ // Ignore trailing non-JSON data
526
+ }
527
+ }
528
+ }
529
+ catch (err) {
530
+ if (this.abortController.signal.aborted) {
531
+ if (!hasResult) {
532
+ yield {
533
+ type: 'result',
534
+ success: false,
535
+ errors: ['Stream aborted'],
536
+ errorSubtype: 'aborted',
537
+ raw: null,
538
+ };
539
+ }
540
+ return;
541
+ }
542
+ const errorMessage = err instanceof Error ? err.message : 'SSE stream error';
543
+ yield {
544
+ type: 'error',
545
+ message: errorMessage,
546
+ raw: err,
547
+ };
548
+ if (!hasResult) {
549
+ yield {
550
+ type: 'result',
551
+ success: false,
552
+ errors: [errorMessage],
553
+ errorSubtype: 'stream_error',
554
+ raw: null,
555
+ };
556
+ }
557
+ }
558
+ finally {
559
+ reader.releaseLock();
560
+ }
561
+ // If we never got a result event, synthesize a success (stream ended cleanly)
562
+ if (!hasResult) {
563
+ yield {
564
+ type: 'result',
565
+ success: true,
566
+ cost: {
567
+ inputTokens: this.mapperState.totalInputTokens || undefined,
568
+ outputTokens: this.mapperState.totalOutputTokens || undefined,
569
+ numTurns: this.mapperState.turnCount || undefined,
570
+ },
571
+ raw: { streamEnded: true },
572
+ };
573
+ }
574
+ }
575
+ /**
576
+ * Parse a single JSON response (non-streaming message/send).
577
+ * The response contains a complete A2A Task object.
578
+ */
579
+ async *parseSingleResponse(response) {
580
+ let body;
581
+ try {
582
+ body = await response.text();
583
+ }
584
+ catch (err) {
585
+ const errorMessage = err instanceof Error ? err.message : 'Failed to read response body';
586
+ yield {
587
+ type: 'error',
588
+ message: errorMessage,
589
+ raw: err,
590
+ };
591
+ yield {
592
+ type: 'result',
593
+ success: false,
594
+ errors: [errorMessage],
595
+ errorSubtype: 'response_error',
596
+ raw: null,
597
+ };
598
+ return;
599
+ }
600
+ let rpcResponse;
601
+ try {
602
+ rpcResponse = JSON.parse(body);
603
+ }
604
+ catch {
605
+ yield {
606
+ type: 'error',
607
+ message: `Invalid JSON response from A2A agent: ${body.slice(0, 200)}`,
608
+ raw: body,
609
+ };
610
+ yield {
611
+ type: 'result',
612
+ success: false,
613
+ errors: ['Invalid JSON response from A2A agent'],
614
+ errorSubtype: 'parse_error',
615
+ raw: null,
616
+ };
617
+ return;
618
+ }
619
+ // Check for JSON-RPC error
620
+ if (rpcResponse.error) {
621
+ yield {
622
+ type: 'error',
623
+ message: rpcResponse.error.message,
624
+ code: String(rpcResponse.error.code),
625
+ raw: rpcResponse.error,
626
+ };
627
+ yield {
628
+ type: 'result',
629
+ success: false,
630
+ errors: [rpcResponse.error.message],
631
+ errorSubtype: 'jsonrpc_error',
632
+ raw: rpcResponse,
633
+ };
634
+ return;
635
+ }
636
+ // Parse the result as an A2A Task
637
+ const task = rpcResponse.result;
638
+ if (!task) {
639
+ yield {
640
+ type: 'error',
641
+ message: 'A2A response has no result',
642
+ raw: rpcResponse,
643
+ };
644
+ yield {
645
+ type: 'result',
646
+ success: false,
647
+ errors: ['A2A response has no result'],
648
+ errorSubtype: 'empty_result',
649
+ raw: rpcResponse,
650
+ };
651
+ return;
652
+ }
653
+ // Set session/task IDs
654
+ this.mapperState.taskId = task.id;
655
+ this.mapperState.sessionId = task.sessionId ?? task.id;
656
+ this.sessionId = this.mapperState.sessionId;
657
+ // Emit init event
658
+ yield {
659
+ type: 'init',
660
+ sessionId: this.mapperState.sessionId,
661
+ raw: task,
662
+ };
663
+ // Emit artifact text if present
664
+ if (task.artifacts) {
665
+ for (const artifact of task.artifacts) {
666
+ const text = extractTextFromParts(artifact.parts);
667
+ if (text) {
668
+ yield {
669
+ type: 'assistant_text',
670
+ text,
671
+ raw: artifact,
672
+ };
673
+ }
674
+ }
675
+ }
676
+ // Emit status message text if present
677
+ if (task.status.message && task.status.message.parts.length > 0) {
678
+ const statusText = extractTextFromParts(task.status.message.parts);
679
+ if (statusText) {
680
+ yield {
681
+ type: 'assistant_text',
682
+ text: statusText,
683
+ raw: task.status,
684
+ };
685
+ }
686
+ }
687
+ // Emit final result based on task status
688
+ switch (task.status.state) {
689
+ case 'completed':
690
+ yield {
691
+ type: 'result',
692
+ success: true,
693
+ message: task.artifacts
694
+ ? extractTextFromParts(task.artifacts[task.artifacts.length - 1].parts)
695
+ : undefined,
696
+ cost: {
697
+ inputTokens: this.mapperState.totalInputTokens || undefined,
698
+ outputTokens: this.mapperState.totalOutputTokens || undefined,
699
+ numTurns: this.mapperState.turnCount || undefined,
700
+ },
701
+ raw: task,
702
+ };
703
+ break;
704
+ case 'failed':
705
+ yield {
706
+ type: 'result',
707
+ success: false,
708
+ errors: [task.status.message
709
+ ? extractTextFromParts(task.status.message.parts)
710
+ : 'Task failed'],
711
+ errorSubtype: 'task_failed',
712
+ raw: task,
713
+ };
714
+ break;
715
+ case 'canceled':
716
+ yield {
717
+ type: 'result',
718
+ success: false,
719
+ errors: [task.status.message
720
+ ? extractTextFromParts(task.status.message.parts)
721
+ : 'Task canceled'],
722
+ errorSubtype: 'canceled',
723
+ raw: task,
724
+ };
725
+ break;
726
+ case 'input-required':
727
+ yield {
728
+ type: 'system',
729
+ subtype: 'input_required',
730
+ message: task.status.message
731
+ ? extractTextFromParts(task.status.message.parts)
732
+ : 'Agent requires additional input',
733
+ raw: task,
734
+ };
735
+ break;
736
+ default:
737
+ // For submitted/working states, emit as system event
738
+ yield {
739
+ type: 'result',
740
+ success: true,
741
+ cost: {
742
+ inputTokens: this.mapperState.totalInputTokens || undefined,
743
+ outputTokens: this.mapperState.totalOutputTokens || undefined,
744
+ numTurns: this.mapperState.turnCount || undefined,
745
+ },
746
+ raw: task,
747
+ };
748
+ break;
749
+ }
750
+ }
751
+ }
752
+ // ---------------------------------------------------------------------------
753
+ // Helpers
754
+ // ---------------------------------------------------------------------------
755
+ /**
756
+ * Resolve the A2A agent URL from environment variables.
757
+ * Checks work-type-specific overrides first, then falls back to the base URL.
758
+ *
759
+ * Resolution order:
760
+ * 1. A2A_AGENT_URL_{WORKTYPE} from config.env
761
+ * 2. A2A_AGENT_URL from config.env
762
+ * 3. A2A_AGENT_URL_{WORKTYPE} from process.env
763
+ * 4. A2A_AGENT_URL from process.env
764
+ */
765
+ function resolveAgentUrl(env) {
766
+ // Check for work-type-specific URL in config env
767
+ for (const [key, value] of Object.entries(env)) {
768
+ if (key.startsWith('A2A_AGENT_URL_') && value) {
769
+ return value;
770
+ }
771
+ }
772
+ // Check base URL in config env
773
+ if (env.A2A_AGENT_URL) {
774
+ return env.A2A_AGENT_URL;
775
+ }
776
+ // Check process.env for work-type-specific
777
+ for (const [key, value] of Object.entries(process.env)) {
778
+ if (key.startsWith('A2A_AGENT_URL_') && value) {
779
+ return value;
780
+ }
781
+ }
782
+ // Check process.env base URL
783
+ return process.env.A2A_AGENT_URL;
784
+ }
785
+ /**
786
+ * Resolve authentication headers from environment variables.
787
+ * Supports API key and bearer token authentication.
788
+ */
789
+ function resolveAuthHeaders(env) {
790
+ const headers = {};
791
+ // API key auth
792
+ const apiKey = env.A2A_API_KEY || process.env.A2A_API_KEY;
793
+ if (apiKey) {
794
+ headers['x-api-key'] = apiKey;
795
+ }
796
+ // Bearer token auth
797
+ const bearerToken = env.A2A_BEARER_TOKEN || process.env.A2A_BEARER_TOKEN;
798
+ if (bearerToken) {
799
+ headers['Authorization'] = `Bearer ${bearerToken}`;
800
+ }
801
+ return headers;
802
+ }
803
+ // ---------------------------------------------------------------------------
804
+ // Factory
805
+ // ---------------------------------------------------------------------------
806
+ /**
807
+ * Create a new A2A provider instance.
808
+ */
809
+ export function createA2aProvider() {
810
+ return new A2aProvider();
811
+ }