@zhijiewang/openharness 1.0.0 → 1.3.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.
- package/README.md +741 -741
- package/dist/agents/roles.d.ts +4 -2
- package/dist/agents/roles.js +69 -5
- package/dist/commands/index.js +87 -29
- package/dist/harness/config.d.ts +17 -0
- package/dist/harness/marketplace.d.ts +62 -0
- package/dist/harness/marketplace.js +242 -0
- package/dist/harness/plugins.d.ts +1 -1
- package/dist/harness/plugins.js +15 -1
- package/dist/harness/telemetry.d.ts +59 -0
- package/dist/harness/telemetry.js +129 -0
- package/dist/main.js +40 -40
- package/dist/providers/router.d.ts +48 -0
- package/dist/providers/router.js +61 -0
- package/dist/query/compress.d.ts +5 -0
- package/dist/query/compress.js +45 -4
- package/dist/remote/auth.d.ts +25 -0
- package/dist/remote/auth.js +73 -0
- package/dist/remote/server.d.ts +18 -2
- package/dist/remote/server.js +168 -39
- package/dist/repl.js +8 -0
- package/dist/services/PipelineExecutor.d.ts +48 -0
- package/dist/services/PipelineExecutor.js +179 -0
- package/dist/services/a2a.d.ts +119 -0
- package/dist/services/a2a.js +176 -0
- package/dist/tools/PipelineTool/index.d.ts +40 -0
- package/dist/tools/PipelineTool/index.js +53 -0
- package/dist/tools/WebFetchTool/index.js +2 -2
- package/dist/tools.js +3 -0
- package/package.json +73 -73
package/dist/remote/server.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Remote server — HTTP + WebSocket server for remote agent dispatch,
|
|
3
|
-
* bidirectional channels, and structured event streaming.
|
|
3
|
+
* bidirectional channels, A2A protocol, and structured event streaming.
|
|
4
4
|
*
|
|
5
5
|
* Endpoints:
|
|
6
|
-
* - POST /dispatch — send a prompt, get a streaming response
|
|
6
|
+
* - POST /dispatch — send a prompt, get a streaming response (SSE)
|
|
7
|
+
* - POST /a2a — A2A protocol: task delegation, discovery, status
|
|
7
8
|
* - GET /status — check server status
|
|
8
9
|
* - WS /channel — bidirectional WebSocket channel
|
|
10
|
+
*
|
|
11
|
+
* Security: bearer token auth, per-IP rate limiting, tool allowlists.
|
|
9
12
|
*/
|
|
10
13
|
import { createServer } from 'node:http';
|
|
11
14
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
15
|
+
import { authenticateRequest, filterRemoteTools } from './auth.js';
|
|
16
|
+
import { createSessionCard, publishCard, unpublishCard, discoverAgents, generateMessageId, } from '../services/a2a.js';
|
|
12
17
|
export class RemoteServer {
|
|
13
18
|
config;
|
|
14
19
|
channels = new Map();
|
|
15
20
|
server = null;
|
|
21
|
+
agentCardId = null;
|
|
16
22
|
constructor(config) {
|
|
17
23
|
this.config = config;
|
|
18
24
|
}
|
|
@@ -33,12 +39,27 @@ export class RemoteServer {
|
|
|
33
39
|
});
|
|
34
40
|
this.server.listen(this.config.port, () => {
|
|
35
41
|
process.stderr.write(`[remote] Server listening on http://localhost:${this.config.port}\n`);
|
|
36
|
-
process.stderr.write(`[remote] Endpoints: POST /dispatch, GET /status, WS /channel\n`);
|
|
42
|
+
process.stderr.write(`[remote] Endpoints: POST /dispatch, POST /a2a, GET /status, WS /channel\n`);
|
|
43
|
+
// Publish A2A agent card with HTTP endpoint
|
|
44
|
+
const sessionId = this.config.sessionId ?? Date.now().toString(36);
|
|
45
|
+
const card = createSessionCard(sessionId, {
|
|
46
|
+
provider: this.config.provider.name,
|
|
47
|
+
model: this.config.model,
|
|
48
|
+
port: this.config.port,
|
|
49
|
+
});
|
|
50
|
+
publishCard(card);
|
|
51
|
+
this.agentCardId = card.id;
|
|
52
|
+
process.stderr.write(`[remote] A2A agent card published: ${card.id}\n`);
|
|
37
53
|
resolve();
|
|
38
54
|
});
|
|
39
55
|
});
|
|
40
56
|
}
|
|
41
57
|
stop() {
|
|
58
|
+
// Unpublish A2A card
|
|
59
|
+
if (this.agentCardId) {
|
|
60
|
+
unpublishCard(this.agentCardId);
|
|
61
|
+
this.agentCardId = null;
|
|
62
|
+
}
|
|
42
63
|
for (const ch of this.channels.values()) {
|
|
43
64
|
ch.abortController.abort();
|
|
44
65
|
ch.ws.close();
|
|
@@ -50,12 +71,23 @@ export class RemoteServer {
|
|
|
50
71
|
// CORS headers
|
|
51
72
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
52
73
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
53
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
74
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
54
75
|
if (req.method === 'OPTIONS') {
|
|
55
76
|
res.writeHead(204);
|
|
56
77
|
res.end();
|
|
57
78
|
return;
|
|
58
79
|
}
|
|
80
|
+
// Auth check (skip for /status which is a health check)
|
|
81
|
+
if (req.url !== '/status') {
|
|
82
|
+
const auth = authenticateRequest(req, res);
|
|
83
|
+
if (!auth.allowed) {
|
|
84
|
+
const status = auth.reason?.includes('Rate limit') ? 429 : 401;
|
|
85
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
86
|
+
res.end(JSON.stringify({ error: auth.reason, requestId: auth.requestId }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── GET /status ──
|
|
59
91
|
if (req.url === '/status' && req.method === 'GET') {
|
|
60
92
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
93
|
res.end(JSON.stringify({
|
|
@@ -63,50 +95,146 @@ export class RemoteServer {
|
|
|
63
95
|
provider: this.config.provider.name,
|
|
64
96
|
model: this.config.model,
|
|
65
97
|
channels: this.channels.size,
|
|
98
|
+
agentId: this.agentCardId,
|
|
66
99
|
}));
|
|
67
100
|
return;
|
|
68
101
|
}
|
|
102
|
+
// ── POST /dispatch ──
|
|
69
103
|
if (req.url === '/dispatch' && req.method === 'POST') {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
await this.handleDispatch(req, res);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// ── POST /a2a ──
|
|
108
|
+
if (req.url === '/a2a' && req.method === 'POST') {
|
|
109
|
+
await this.handleA2A(req, res);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
113
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
114
|
+
}
|
|
115
|
+
async handleDispatch(req, res) {
|
|
116
|
+
const body = await readBody(req);
|
|
117
|
+
try {
|
|
118
|
+
const { prompt, maxTurns } = JSON.parse(body);
|
|
119
|
+
if (!prompt) {
|
|
120
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ error: 'Missing "prompt" field' }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Apply tool filtering for remote callers
|
|
125
|
+
const tools = filterRemoteTools(this.config.tools);
|
|
126
|
+
// Stream response as Server-Sent Events
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
'Content-Type': 'text/event-stream',
|
|
129
|
+
'Cache-Control': 'no-cache',
|
|
130
|
+
'Connection': 'keep-alive',
|
|
131
|
+
});
|
|
132
|
+
const { query } = await import('../query.js');
|
|
133
|
+
const config = {
|
|
134
|
+
provider: this.config.provider,
|
|
135
|
+
tools,
|
|
136
|
+
systemPrompt: this.config.systemPrompt,
|
|
137
|
+
permissionMode: this.config.permissionMode,
|
|
138
|
+
model: this.config.model,
|
|
139
|
+
maxTurns: maxTurns ?? 20,
|
|
140
|
+
};
|
|
141
|
+
for await (const event of query(prompt, config)) {
|
|
142
|
+
const data = JSON.stringify(event);
|
|
143
|
+
res.write(`data: ${data}\n\n`);
|
|
144
|
+
}
|
|
145
|
+
res.write('data: [DONE]\n\n');
|
|
146
|
+
res.end();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!res.headersSent) {
|
|
150
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
151
|
+
}
|
|
152
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A2A protocol handler — receives inter-agent messages.
|
|
157
|
+
*
|
|
158
|
+
* Supports:
|
|
159
|
+
* - task: delegate a task to this agent
|
|
160
|
+
* - discover: return this agent's capabilities
|
|
161
|
+
* - status: return current state
|
|
162
|
+
* - cancel: abort a running task
|
|
163
|
+
*/
|
|
164
|
+
async handleA2A(req, res) {
|
|
165
|
+
const body = await readBody(req);
|
|
166
|
+
try {
|
|
167
|
+
const message = JSON.parse(body);
|
|
168
|
+
switch (message.payload.kind) {
|
|
169
|
+
case 'discover': {
|
|
170
|
+
// Return our agent card
|
|
171
|
+
const agents = discoverAgents();
|
|
172
|
+
const self = agents.find(a => a.id === this.agentCardId);
|
|
173
|
+
const response = {
|
|
174
|
+
id: generateMessageId(),
|
|
175
|
+
from: this.agentCardId ?? 'unknown',
|
|
176
|
+
to: message.from,
|
|
177
|
+
type: 'result',
|
|
178
|
+
payload: { kind: 'result', taskId: message.id, output: self ?? { error: 'agent not found' } },
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
};
|
|
181
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
182
|
+
res.end(JSON.stringify(response));
|
|
76
183
|
return;
|
|
77
184
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
185
|
+
case 'task': {
|
|
186
|
+
// Execute the task via query loop
|
|
187
|
+
const tools = filterRemoteTools(this.config.tools);
|
|
188
|
+
const { query } = await import('../query.js');
|
|
189
|
+
const config = {
|
|
190
|
+
provider: this.config.provider,
|
|
191
|
+
tools,
|
|
192
|
+
systemPrompt: `[A2A Task from agent ${message.from}]\n\n${this.config.systemPrompt}`,
|
|
193
|
+
permissionMode: this.config.permissionMode,
|
|
194
|
+
model: this.config.model,
|
|
195
|
+
maxTurns: 10,
|
|
196
|
+
};
|
|
197
|
+
let output = '';
|
|
198
|
+
for await (const event of query(String(message.payload.input), config)) {
|
|
199
|
+
if (event.type === 'text_delta')
|
|
200
|
+
output += event.content;
|
|
201
|
+
}
|
|
202
|
+
const response = {
|
|
203
|
+
id: generateMessageId(),
|
|
204
|
+
from: this.agentCardId ?? 'unknown',
|
|
205
|
+
to: message.from,
|
|
206
|
+
type: 'result',
|
|
207
|
+
payload: { kind: 'result', taskId: message.id, output },
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
};
|
|
210
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
211
|
+
res.end(JSON.stringify(response));
|
|
212
|
+
return;
|
|
96
213
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
214
|
+
case 'status': {
|
|
215
|
+
const response = {
|
|
216
|
+
id: generateMessageId(),
|
|
217
|
+
from: this.agentCardId ?? 'unknown',
|
|
218
|
+
to: message.from,
|
|
219
|
+
type: 'status',
|
|
220
|
+
payload: { kind: 'status', state: 'idle' },
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
};
|
|
223
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
224
|
+
res.end(JSON.stringify(response));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
default: {
|
|
228
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
229
|
+
res.end(JSON.stringify({ error: `Unknown A2A message kind: ${message.payload.kind}` }));
|
|
230
|
+
return;
|
|
103
231
|
}
|
|
104
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
105
232
|
}
|
|
106
|
-
return;
|
|
107
233
|
}
|
|
108
|
-
|
|
109
|
-
|
|
234
|
+
catch (err) {
|
|
235
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
237
|
+
}
|
|
110
238
|
}
|
|
111
239
|
handleChannel(ws) {
|
|
112
240
|
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
@@ -119,10 +247,11 @@ export class RemoteServer {
|
|
|
119
247
|
try {
|
|
120
248
|
const msg = JSON.parse(data.toString());
|
|
121
249
|
if (msg.type === 'dispatch') {
|
|
250
|
+
const tools = filterRemoteTools(this.config.tools);
|
|
122
251
|
const { query } = await import('../query.js');
|
|
123
252
|
const config = {
|
|
124
253
|
provider: this.config.provider,
|
|
125
|
-
tools
|
|
254
|
+
tools,
|
|
126
255
|
systemPrompt: this.config.systemPrompt,
|
|
127
256
|
permissionMode: this.config.permissionMode,
|
|
128
257
|
model: this.config.model,
|
package/dist/repl.js
CHANGED
|
@@ -62,6 +62,13 @@ export async function startREPL(config) {
|
|
|
62
62
|
const { CronExecutor } = await import('./services/CronExecutor.js');
|
|
63
63
|
const cronExecutor = new CronExecutor(config.provider, config.tools, config.systemPrompt, config.permissionMode, config.model);
|
|
64
64
|
cronExecutor.start();
|
|
65
|
+
// A2A: publish agent card for cross-process discovery
|
|
66
|
+
const { createSessionCard, publishCard, unpublishCard } = await import('./services/a2a.js');
|
|
67
|
+
const agentCard = createSessionCard(session.id, {
|
|
68
|
+
provider: config.provider.name,
|
|
69
|
+
model: config.model,
|
|
70
|
+
});
|
|
71
|
+
publishCard(agentCard);
|
|
65
72
|
const cost = new CostTracker();
|
|
66
73
|
let cachedConfig = readOhConfig();
|
|
67
74
|
// Centralized state store — all REPL state lives here
|
|
@@ -923,6 +930,7 @@ export async function startREPL(config) {
|
|
|
923
930
|
if (cleanedUp)
|
|
924
931
|
return;
|
|
925
932
|
cleanedUp = true;
|
|
933
|
+
unpublishCard(agentCard.id);
|
|
926
934
|
cronExecutor.stop();
|
|
927
935
|
renderer.stop();
|
|
928
936
|
session.messages = messages;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PipelineExecutor — declarative multi-step tool workflows.
|
|
3
|
+
*
|
|
4
|
+
* Executes a sequence of tool calls with dependency resolution and
|
|
5
|
+
* variable substitution. Steps can reference prior step outputs via $stepId.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the LLM-mediated agent loop, pipelines are deterministic —
|
|
8
|
+
* faster, cheaper, and repeatable for known workflows.
|
|
9
|
+
*
|
|
10
|
+
* Reuses the dependency resolution pattern from AgentDispatcher.
|
|
11
|
+
*/
|
|
12
|
+
import type { Tools, ToolContext } from '../Tool.js';
|
|
13
|
+
export type PipelineStep = {
|
|
14
|
+
id: string;
|
|
15
|
+
tool: string;
|
|
16
|
+
args: Record<string, unknown>;
|
|
17
|
+
dependsOn?: string[];
|
|
18
|
+
};
|
|
19
|
+
export type PipelineStepResult = {
|
|
20
|
+
stepId: string;
|
|
21
|
+
output: string;
|
|
22
|
+
isError: boolean;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
};
|
|
25
|
+
export declare class PipelineExecutor {
|
|
26
|
+
private tools;
|
|
27
|
+
private context;
|
|
28
|
+
constructor(tools: Tools, context: ToolContext);
|
|
29
|
+
/**
|
|
30
|
+
* Execute a pipeline. Returns results for all steps.
|
|
31
|
+
* Steps with unmet dependencies (failed/skipped blockers) are skipped.
|
|
32
|
+
*/
|
|
33
|
+
execute(steps: PipelineStep[]): Promise<PipelineStepResult[]>;
|
|
34
|
+
private isReady;
|
|
35
|
+
private hasFailedBlocker;
|
|
36
|
+
private executeStep;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve $stepId references in args.
|
|
39
|
+
* If a string value starts with $, replace it with the output of that step.
|
|
40
|
+
* Supports nested objects and arrays.
|
|
41
|
+
*/
|
|
42
|
+
private resolveArgs;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format pipeline results as a readable summary.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatPipelineResults(results: PipelineStepResult[]): string;
|
|
48
|
+
//# sourceMappingURL=PipelineExecutor.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PipelineExecutor — declarative multi-step tool workflows.
|
|
3
|
+
*
|
|
4
|
+
* Executes a sequence of tool calls with dependency resolution and
|
|
5
|
+
* variable substitution. Steps can reference prior step outputs via $stepId.
|
|
6
|
+
*
|
|
7
|
+
* Unlike the LLM-mediated agent loop, pipelines are deterministic —
|
|
8
|
+
* faster, cheaper, and repeatable for known workflows.
|
|
9
|
+
*
|
|
10
|
+
* Reuses the dependency resolution pattern from AgentDispatcher.
|
|
11
|
+
*/
|
|
12
|
+
import { findToolByName } from '../Tool.js';
|
|
13
|
+
// ── Executor ──
|
|
14
|
+
export class PipelineExecutor {
|
|
15
|
+
tools;
|
|
16
|
+
context;
|
|
17
|
+
constructor(tools, context) {
|
|
18
|
+
this.tools = tools;
|
|
19
|
+
this.context = context;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute a pipeline. Returns results for all steps.
|
|
23
|
+
* Steps with unmet dependencies (failed/skipped blockers) are skipped.
|
|
24
|
+
*/
|
|
25
|
+
async execute(steps) {
|
|
26
|
+
// Validate step IDs are unique
|
|
27
|
+
const ids = new Set(steps.map(s => s.id));
|
|
28
|
+
if (ids.size !== steps.length) {
|
|
29
|
+
return [{ stepId: 'pipeline', output: 'Error: duplicate step IDs', isError: true, durationMs: 0 }];
|
|
30
|
+
}
|
|
31
|
+
const internal = new Map();
|
|
32
|
+
for (const step of steps) {
|
|
33
|
+
internal.set(step.id, { ...step, status: 'pending' });
|
|
34
|
+
}
|
|
35
|
+
const results = [];
|
|
36
|
+
// Process steps in dependency order
|
|
37
|
+
while (true) {
|
|
38
|
+
const ready = [...internal.values()].filter(s => s.status === 'pending' && this.isReady(s, internal));
|
|
39
|
+
const running = [...internal.values()].filter(s => s.status === 'running');
|
|
40
|
+
if (ready.length === 0 && running.length === 0)
|
|
41
|
+
break;
|
|
42
|
+
// Execute ready steps (sequentially for safety — tools may have side effects)
|
|
43
|
+
for (const step of ready) {
|
|
44
|
+
step.status = 'running';
|
|
45
|
+
// Check if any blocker failed — skip this step
|
|
46
|
+
if (this.hasFailedBlocker(step, internal)) {
|
|
47
|
+
step.status = 'skipped';
|
|
48
|
+
const result = {
|
|
49
|
+
stepId: step.id,
|
|
50
|
+
output: 'Skipped: dependency failed',
|
|
51
|
+
isError: true,
|
|
52
|
+
durationMs: 0,
|
|
53
|
+
};
|
|
54
|
+
step.result = result;
|
|
55
|
+
results.push(result);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const result = await this.executeStep(step, internal);
|
|
59
|
+
step.result = result;
|
|
60
|
+
step.status = result.isError ? 'failed' : 'completed';
|
|
61
|
+
results.push(result);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
isReady(step, all) {
|
|
67
|
+
if (!step.dependsOn || step.dependsOn.length === 0)
|
|
68
|
+
return true;
|
|
69
|
+
return step.dependsOn.every(id => {
|
|
70
|
+
const dep = all.get(id);
|
|
71
|
+
return dep && (dep.status === 'completed' || dep.status === 'failed' || dep.status === 'skipped');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
hasFailedBlocker(step, all) {
|
|
75
|
+
if (!step.dependsOn)
|
|
76
|
+
return false;
|
|
77
|
+
return step.dependsOn.some(id => {
|
|
78
|
+
const dep = all.get(id);
|
|
79
|
+
return dep && (dep.status === 'failed' || dep.status === 'skipped');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async executeStep(step, all) {
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
// Find the tool
|
|
85
|
+
const tool = findToolByName(this.tools, step.tool);
|
|
86
|
+
if (!tool) {
|
|
87
|
+
return {
|
|
88
|
+
stepId: step.id,
|
|
89
|
+
output: `Error: unknown tool '${step.tool}'`,
|
|
90
|
+
isError: true,
|
|
91
|
+
durationMs: Date.now() - start,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Substitute $refs in args
|
|
95
|
+
const resolvedArgs = this.resolveArgs(step.args, all);
|
|
96
|
+
// Validate and execute
|
|
97
|
+
const parsed = tool.inputSchema.safeParse(resolvedArgs);
|
|
98
|
+
if (!parsed.success) {
|
|
99
|
+
return {
|
|
100
|
+
stepId: step.id,
|
|
101
|
+
output: `Validation error: ${parsed.error.message}`,
|
|
102
|
+
isError: true,
|
|
103
|
+
durationMs: Date.now() - start,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = await tool.call(parsed.data, this.context);
|
|
108
|
+
return {
|
|
109
|
+
stepId: step.id,
|
|
110
|
+
output: result.output,
|
|
111
|
+
isError: result.isError,
|
|
112
|
+
durationMs: Date.now() - start,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return {
|
|
117
|
+
stepId: step.id,
|
|
118
|
+
output: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
119
|
+
isError: true,
|
|
120
|
+
durationMs: Date.now() - start,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve $stepId references in args.
|
|
126
|
+
* If a string value starts with $, replace it with the output of that step.
|
|
127
|
+
* Supports nested objects and arrays.
|
|
128
|
+
*/
|
|
129
|
+
resolveArgs(args, all) {
|
|
130
|
+
const resolve = (value) => {
|
|
131
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
132
|
+
const refId = value.slice(1);
|
|
133
|
+
const refStep = all.get(refId);
|
|
134
|
+
if (refStep?.result && !refStep.result.isError) {
|
|
135
|
+
return refStep.result.output;
|
|
136
|
+
}
|
|
137
|
+
return value; // Keep as-is if ref not found
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(value))
|
|
140
|
+
return value.map(resolve);
|
|
141
|
+
if (value && typeof value === 'object') {
|
|
142
|
+
const resolved = {};
|
|
143
|
+
for (const [k, v] of Object.entries(value)) {
|
|
144
|
+
resolved[k] = resolve(v);
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
};
|
|
150
|
+
return resolve(args);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Format pipeline results as a readable summary.
|
|
155
|
+
*/
|
|
156
|
+
export function formatPipelineResults(results) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
let totalMs = 0;
|
|
159
|
+
for (const r of results) {
|
|
160
|
+
const status = r.isError ? '✗' : '✓';
|
|
161
|
+
const duration = r.durationMs > 0 ? ` (${r.durationMs}ms)` : '';
|
|
162
|
+
lines.push(`${status} Step "${r.stepId}"${duration}`);
|
|
163
|
+
// Show truncated output
|
|
164
|
+
const output = r.output.length > 200
|
|
165
|
+
? r.output.slice(0, 200) + '...'
|
|
166
|
+
: r.output;
|
|
167
|
+
if (output) {
|
|
168
|
+
for (const line of output.split('\n').slice(0, 5)) {
|
|
169
|
+
lines.push(` ${line}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
totalMs += r.durationMs;
|
|
174
|
+
}
|
|
175
|
+
const passed = results.filter(r => !r.isError).length;
|
|
176
|
+
lines.push(`Pipeline: ${passed}/${results.length} steps passed (${totalMs}ms total)`);
|
|
177
|
+
return lines.join('\n');
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=PipelineExecutor.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Protocol — Agent-to-Agent discovery and routing.
|
|
3
|
+
*
|
|
4
|
+
* Enables agents running in separate processes (or machines) to:
|
|
5
|
+
* - Advertise their capabilities via Agent Cards
|
|
6
|
+
* - Discover other agents via a shared registry
|
|
7
|
+
* - Route messages to agents by name or capability
|
|
8
|
+
* - Delegate tasks with typed request/response
|
|
9
|
+
*
|
|
10
|
+
* Registry is file-based (~/.oh/agents/) for same-machine agents.
|
|
11
|
+
* Each running agent writes a card file on startup and removes it on exit.
|
|
12
|
+
*
|
|
13
|
+
* Based on the emerging A2A (Agent-to-Agent) protocol standard.
|
|
14
|
+
*/
|
|
15
|
+
export type AgentCard = {
|
|
16
|
+
/** Unique agent instance ID */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Human-readable name */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Agent version */
|
|
21
|
+
version: string;
|
|
22
|
+
/** What this agent can do */
|
|
23
|
+
capabilities: AgentCapability[];
|
|
24
|
+
/** How to reach this agent */
|
|
25
|
+
endpoint: AgentEndpoint;
|
|
26
|
+
/** When this card was published */
|
|
27
|
+
registeredAt: number;
|
|
28
|
+
/** PID of the agent process */
|
|
29
|
+
pid: number;
|
|
30
|
+
/** Provider and model info */
|
|
31
|
+
provider?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
/** Working directory */
|
|
34
|
+
workingDir?: string;
|
|
35
|
+
};
|
|
36
|
+
export type AgentCapability = {
|
|
37
|
+
/** Capability identifier (e.g., 'code-review', 'test-generation') */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Human description */
|
|
40
|
+
description: string;
|
|
41
|
+
/** Input schema (JSON Schema format) */
|
|
42
|
+
inputSchema?: Record<string, unknown>;
|
|
43
|
+
/** Output schema */
|
|
44
|
+
outputSchema?: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
export type AgentEndpoint = {
|
|
47
|
+
/** Transport type */
|
|
48
|
+
type: 'http' | 'ipc' | 'stdio';
|
|
49
|
+
/** Address (URL for http, socket path for ipc, pid for stdio) */
|
|
50
|
+
address: string;
|
|
51
|
+
/** Port for HTTP transport */
|
|
52
|
+
port?: number;
|
|
53
|
+
};
|
|
54
|
+
export type A2AMessage = {
|
|
55
|
+
/** Message ID */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Source agent ID */
|
|
58
|
+
from: string;
|
|
59
|
+
/** Target agent ID or capability name */
|
|
60
|
+
to: string;
|
|
61
|
+
/** Message type */
|
|
62
|
+
type: 'task' | 'result' | 'status' | 'cancel' | 'discover';
|
|
63
|
+
/** Payload */
|
|
64
|
+
payload: A2APayload;
|
|
65
|
+
/** Timestamp */
|
|
66
|
+
timestamp: number;
|
|
67
|
+
};
|
|
68
|
+
export type A2APayload = {
|
|
69
|
+
kind: 'task';
|
|
70
|
+
capability: string;
|
|
71
|
+
input: unknown;
|
|
72
|
+
timeout?: number;
|
|
73
|
+
} | {
|
|
74
|
+
kind: 'result';
|
|
75
|
+
taskId: string;
|
|
76
|
+
output: unknown;
|
|
77
|
+
error?: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: 'status';
|
|
80
|
+
state: 'idle' | 'working' | 'done' | 'error';
|
|
81
|
+
progress?: string;
|
|
82
|
+
} | {
|
|
83
|
+
kind: 'cancel';
|
|
84
|
+
taskId: string;
|
|
85
|
+
reason?: string;
|
|
86
|
+
} | {
|
|
87
|
+
kind: 'discover';
|
|
88
|
+
filter?: {
|
|
89
|
+
capability?: string;
|
|
90
|
+
name?: string;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
/** Publish an agent card to the shared registry */
|
|
94
|
+
export declare function publishCard(card: AgentCard): void;
|
|
95
|
+
/** Remove an agent card from the registry */
|
|
96
|
+
export declare function unpublishCard(agentId: string): void;
|
|
97
|
+
/** Discover all registered agents */
|
|
98
|
+
export declare function discoverAgents(): AgentCard[];
|
|
99
|
+
/** Find agents by capability name */
|
|
100
|
+
export declare function findAgentsByCapability(capabilityName: string): AgentCard[];
|
|
101
|
+
/** Find an agent by name */
|
|
102
|
+
export declare function findAgentByName(name: string): AgentCard | null;
|
|
103
|
+
/**
|
|
104
|
+
* Route a message to an agent.
|
|
105
|
+
* For HTTP endpoints: sends via fetch.
|
|
106
|
+
* For IPC/stdio: writes to the agent's inbox file.
|
|
107
|
+
*/
|
|
108
|
+
export declare function routeMessage(message: A2AMessage): Promise<A2AMessage | null>;
|
|
109
|
+
/** Read pending messages from an agent's inbox */
|
|
110
|
+
export declare function readInbox(agentId: string): A2AMessage[];
|
|
111
|
+
/** Generate a unique message ID */
|
|
112
|
+
export declare function generateMessageId(): string;
|
|
113
|
+
/** Create a standard agent card for the current openHarness session */
|
|
114
|
+
export declare function createSessionCard(sessionId: string, opts?: {
|
|
115
|
+
provider?: string;
|
|
116
|
+
model?: string;
|
|
117
|
+
port?: number;
|
|
118
|
+
}): AgentCard;
|
|
119
|
+
//# sourceMappingURL=a2a.d.ts.map
|