opc-agent 1.2.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/CHANGELOG.md +6 -0
- package/README.md +306 -429
- package/package.json +1 -1
- package/src/cli.ts +119 -1
- package/src/core/streaming.ts +195 -0
- package/src/deploy/openclaw.ts +10 -10
- package/src/index.ts +12 -2
- package/src/schema/oad.ts +1 -2
- package/src/templates/code-reviewer.ts +0 -4
- package/src/templates/customer-service.ts +0 -4
- package/src/templates/data-analyst.ts +0 -4
- package/src/templates/knowledge-base.ts +0 -4
- package/src/templates/sales-assistant.ts +0 -4
- package/src/templates/teacher.ts +0 -4
- package/src/tools/gateway.ts +220 -0
- package/src/traces/index.ts +132 -0
- package/tests/gateway.test.ts +71 -0
- package/tests/streaming.test.ts +109 -0
- package/src/dtv/data.ts +0 -29
- package/src/dtv/trust.ts +0 -43
- package/src/dtv/value.ts +0 -47
- package/src/marketplace/index.ts +0 -223
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -27,9 +27,9 @@ import { WorkflowEngine } from './core/workflow';
|
|
|
27
27
|
import { VersionManager } from './core/versioning';
|
|
28
28
|
import { createProvider } from './providers';
|
|
29
29
|
import { KnowledgeBase } from './core/knowledge';
|
|
30
|
-
import { publishAgent, installAgent } from './marketplace';
|
|
31
30
|
|
|
32
31
|
import { PluginManager, createLoggingPlugin, createAnalyticsPlugin, createRateLimitPlugin } from './plugins';
|
|
32
|
+
import type { Span } from './traces';
|
|
33
33
|
|
|
34
34
|
const program = new Command();
|
|
35
35
|
|
|
@@ -873,4 +873,122 @@ program
|
|
|
873
873
|
}
|
|
874
874
|
});
|
|
875
875
|
|
|
876
|
+
// ── Brain command ────────────────────────────────────────────
|
|
877
|
+
|
|
878
|
+
program
|
|
879
|
+
.command('brain')
|
|
880
|
+
.description('Show agent memory/brain status from DeepBrain')
|
|
881
|
+
.option('--url <url>', 'DeepBrain server URL', 'http://localhost:3333')
|
|
882
|
+
.action(async (opts: { url: string }) => {
|
|
883
|
+
console.log(`\n${icon.gear} ${color.bold('DeepBrain Status')} — ${color.dim(opts.url)}\n`);
|
|
884
|
+
try {
|
|
885
|
+
const res = await fetch(`${opts.url}/api/stats`);
|
|
886
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
887
|
+
const stats = (await res.json()) as Record<string, any>;
|
|
888
|
+
const rows: [string, string][] = [
|
|
889
|
+
['Total Pages', String(stats.totalPages ?? stats.pages ?? '-')],
|
|
890
|
+
['Total Chunks', String(stats.totalChunks ?? stats.chunks ?? '-')],
|
|
891
|
+
['Memory Tiers', String(stats.memoryTiers ?? stats.tiers ?? '-')],
|
|
892
|
+
['Index Size', stats.indexSize ?? '-'],
|
|
893
|
+
['Last Updated', stats.lastUpdated ?? stats.updatedAt ?? '-'],
|
|
894
|
+
];
|
|
895
|
+
const maxKey = Math.max(...rows.map(([k]) => k.length));
|
|
896
|
+
for (const [key, val] of rows) {
|
|
897
|
+
console.log(` ${color.cyan(key.padEnd(maxKey))} ${val}`);
|
|
898
|
+
}
|
|
899
|
+
console.log();
|
|
900
|
+
} catch (err) {
|
|
901
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
902
|
+
if (msg.includes('ECONNREFUSED') || msg.includes('fetch failed')) {
|
|
903
|
+
console.log(` ${icon.warn} Cannot connect to DeepBrain at ${opts.url}`);
|
|
904
|
+
console.log(` ${color.dim('Is the server running? Start with: deepbrain serve')}\n`);
|
|
905
|
+
} else {
|
|
906
|
+
console.error(` ${icon.error} ${msg}\n`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// ── Logs command ─────────────────────────────────────────────
|
|
912
|
+
|
|
913
|
+
program
|
|
914
|
+
.command('logs')
|
|
915
|
+
.description('Show recent agent traces')
|
|
916
|
+
.option('-n, --limit <n>', 'Number of spans to show', '20')
|
|
917
|
+
.option('-f, --follow', 'Keep watching for new spans')
|
|
918
|
+
.action(async (opts: { limit: string; follow?: boolean }) => {
|
|
919
|
+
const { TraceCollector } = await import('./traces');
|
|
920
|
+
const collector = new TraceCollector();
|
|
921
|
+
const limit = parseInt(opts.limit) || 20;
|
|
922
|
+
|
|
923
|
+
const printSpans = (spans: readonly Span[]) => {
|
|
924
|
+
const slice = spans.slice(-limit);
|
|
925
|
+
if (slice.length === 0) {
|
|
926
|
+
console.log(` ${icon.info} No traces yet. Interact with the agent to generate traces.`);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
for (const span of slice) {
|
|
930
|
+
const duration = span.endTime
|
|
931
|
+
? `${span.endTime.getTime() - span.startTime.getTime()}ms`
|
|
932
|
+
: 'ongoing';
|
|
933
|
+
const statusIcon = span.status === 'ok' ? icon.success : span.status === 'error' ? icon.error : color.dim('○');
|
|
934
|
+
const time = span.startTime.toLocaleTimeString();
|
|
935
|
+
console.log(` ${statusIcon} ${color.dim(time)} ${color.bold(span.name)} ${color.dim(duration)}`);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
console.log(`\n${icon.gear} ${color.bold('Agent Traces')}\n`);
|
|
940
|
+
const spans = collector.getBufferedSpans();
|
|
941
|
+
printSpans(spans);
|
|
942
|
+
|
|
943
|
+
if (opts.follow) {
|
|
944
|
+
console.log(`\n ${color.dim('Watching for new traces... (Ctrl+C to stop)')}\n`);
|
|
945
|
+
let lastCount = spans.length;
|
|
946
|
+
const interval = setInterval(() => {
|
|
947
|
+
const current = collector.getBufferedSpans();
|
|
948
|
+
if (current.length > lastCount) {
|
|
949
|
+
const newSpans = current.slice(lastCount);
|
|
950
|
+
printSpans(newSpans);
|
|
951
|
+
lastCount = current.length;
|
|
952
|
+
}
|
|
953
|
+
}, 1000);
|
|
954
|
+
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
955
|
+
} else {
|
|
956
|
+
console.log();
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// ── Score command ────────────────────────────────────────────
|
|
961
|
+
|
|
962
|
+
program
|
|
963
|
+
.command('score')
|
|
964
|
+
.description('Show agent performance score')
|
|
965
|
+
.action(async () => {
|
|
966
|
+
console.log(`\n${icon.gear} ${color.bold('Agent Performance Score')}\n`);
|
|
967
|
+
try {
|
|
968
|
+
const engine = new AnalyticsEngine('.');
|
|
969
|
+
const stats = engine.getStats();
|
|
970
|
+
if (!stats || stats.totalMessages === 0) {
|
|
971
|
+
console.log(` ${icon.info} No score data yet. Run the agent first.\n`);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const errorRate = stats.totalMessages > 0 ? (stats.totalErrors / stats.totalMessages) : 0;
|
|
975
|
+
const rows: [string, string][] = [
|
|
976
|
+
['Total Messages', String(stats.totalMessages)],
|
|
977
|
+
['Total LLM Calls', String(stats.totalLLMCalls)],
|
|
978
|
+
['Total Tool Uses', String(stats.totalToolUses)],
|
|
979
|
+
['Avg Response Time', `${stats.avgResponseTimeMs}ms`],
|
|
980
|
+
['Error Rate', `${(errorRate * 100).toFixed(1)}%`],
|
|
981
|
+
['Token Usage', `${stats.totalTokens.total} tokens (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`],
|
|
982
|
+
];
|
|
983
|
+
const maxKey = Math.max(...rows.map(([k]) => k.length));
|
|
984
|
+
for (const [key, val] of rows) {
|
|
985
|
+
console.log(` ${color.cyan(key.padEnd(maxKey))} ${val}`);
|
|
986
|
+
}
|
|
987
|
+
console.log();
|
|
988
|
+
} catch {
|
|
989
|
+
console.log(` ${icon.info} No score data yet. Run the agent first.\n`);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
|
|
876
993
|
program.parse();
|
|
994
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
// ─── Types ───────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface StreamChunk {
|
|
6
|
+
id: string;
|
|
7
|
+
type: 'text' | 'tool_call' | 'error' | 'done';
|
|
8
|
+
data: string;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StreamOptions {
|
|
14
|
+
/** High-water mark for backpressure (default 64 chunks). */
|
|
15
|
+
highWaterMark?: number;
|
|
16
|
+
/** Heartbeat interval in ms to keep connection alive (default 15000). */
|
|
17
|
+
heartbeatInterval?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── StreamableResponse ──────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export class StreamableResponse extends EventEmitter {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
private chunks: StreamChunk[] = [];
|
|
25
|
+
private ended = false;
|
|
26
|
+
private paused = false;
|
|
27
|
+
private buffer: StreamChunk[] = [];
|
|
28
|
+
private highWaterMark: number;
|
|
29
|
+
|
|
30
|
+
constructor(id: string, options?: StreamOptions) {
|
|
31
|
+
super();
|
|
32
|
+
this.id = id;
|
|
33
|
+
this.highWaterMark = options?.highWaterMark ?? 64;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Push a chunk. Returns false if backpressure threshold reached. */
|
|
37
|
+
push(chunk: StreamChunk): boolean {
|
|
38
|
+
if (this.ended) return false;
|
|
39
|
+
|
|
40
|
+
this.chunks.push(chunk);
|
|
41
|
+
|
|
42
|
+
if (this.paused) {
|
|
43
|
+
this.buffer.push(chunk);
|
|
44
|
+
return this.buffer.length < this.highWaterMark;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.emit('chunk', chunk);
|
|
48
|
+
|
|
49
|
+
if (this.chunks.length >= this.highWaterMark) {
|
|
50
|
+
this.paused = true;
|
|
51
|
+
this.emit('backpressure');
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resume after backpressure — flush buffered chunks. */
|
|
58
|
+
resume(): void {
|
|
59
|
+
if (!this.paused) return;
|
|
60
|
+
this.paused = false;
|
|
61
|
+
const buffered = this.buffer.splice(0);
|
|
62
|
+
for (const chunk of buffered) {
|
|
63
|
+
this.emit('chunk', chunk);
|
|
64
|
+
}
|
|
65
|
+
this.emit('drain');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
end(): void {
|
|
69
|
+
if (this.ended) return;
|
|
70
|
+
this.ended = true;
|
|
71
|
+
if (this.paused) this.resume();
|
|
72
|
+
this.emit('end');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getChunks(): StreamChunk[] {
|
|
76
|
+
return [...this.chunks];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Collect all text chunks into a single string. */
|
|
80
|
+
getText(): string {
|
|
81
|
+
return this.chunks
|
|
82
|
+
.filter((c) => c.type === 'text')
|
|
83
|
+
.map((c) => c.data)
|
|
84
|
+
.join('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get isEnded(): boolean {
|
|
88
|
+
return this.ended;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get isPaused(): boolean {
|
|
92
|
+
return this.paused;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get length(): number {
|
|
96
|
+
return this.chunks.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─── StreamingManager ────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export class StreamingManager {
|
|
103
|
+
private streams: Map<string, StreamableResponse> = new Map();
|
|
104
|
+
private counter = 0;
|
|
105
|
+
|
|
106
|
+
/** Create a new stream. */
|
|
107
|
+
createStream(options?: StreamOptions): StreamableResponse {
|
|
108
|
+
const id = `stream_${++this.counter}_${Date.now()}`;
|
|
109
|
+
const stream = new StreamableResponse(id, options);
|
|
110
|
+
this.streams.set(id, stream);
|
|
111
|
+
stream.on('end', () => {
|
|
112
|
+
// Keep ended streams for a bit for late consumers, then clean up
|
|
113
|
+
setTimeout(() => this.streams.delete(id), 30_000);
|
|
114
|
+
});
|
|
115
|
+
return stream;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Write a text chunk to a stream. */
|
|
119
|
+
writeChunk(streamId: string, data: string, metadata?: Record<string, unknown>): boolean {
|
|
120
|
+
const stream = this.streams.get(streamId);
|
|
121
|
+
if (!stream) return false;
|
|
122
|
+
return stream.push({
|
|
123
|
+
id: `chunk_${stream.length}`,
|
|
124
|
+
type: 'text',
|
|
125
|
+
data,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
metadata,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** End a stream. */
|
|
132
|
+
endStream(streamId: string): void {
|
|
133
|
+
const stream = this.streams.get(streamId);
|
|
134
|
+
if (!stream) return;
|
|
135
|
+
stream.push({
|
|
136
|
+
id: `chunk_${stream.length}`,
|
|
137
|
+
type: 'done',
|
|
138
|
+
data: '',
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
stream.end();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Get an existing stream. */
|
|
145
|
+
getStream(streamId: string): StreamableResponse | undefined {
|
|
146
|
+
return this.streams.get(streamId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Format a chunk as an SSE event string. */
|
|
150
|
+
static formatSSE(chunk: StreamChunk): string {
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push(`event: ${chunk.type}`);
|
|
153
|
+
lines.push(`id: ${chunk.id}`);
|
|
154
|
+
const payload = JSON.stringify({ data: chunk.data, metadata: chunk.metadata });
|
|
155
|
+
lines.push(`data: ${payload}`);
|
|
156
|
+
lines.push('');
|
|
157
|
+
return lines.join('\n') + '\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Pipe a stream to an SSE-compatible HTTP response (Express-style). */
|
|
161
|
+
static pipeSSE(
|
|
162
|
+
stream: StreamableResponse,
|
|
163
|
+
res: { write(data: string): boolean; end(): void; setHeader?(name: string, value: string): void },
|
|
164
|
+
options?: StreamOptions,
|
|
165
|
+
): void {
|
|
166
|
+
res.setHeader?.('Content-Type', 'text/event-stream');
|
|
167
|
+
res.setHeader?.('Cache-Control', 'no-cache');
|
|
168
|
+
res.setHeader?.('Connection', 'keep-alive');
|
|
169
|
+
|
|
170
|
+
const heartbeatMs = options?.heartbeatInterval ?? 15_000;
|
|
171
|
+
const heartbeat = setInterval(() => {
|
|
172
|
+
res.write(': heartbeat\n\n');
|
|
173
|
+
}, heartbeatMs);
|
|
174
|
+
|
|
175
|
+
stream.on('chunk', (chunk: StreamChunk) => {
|
|
176
|
+
const ok = res.write(StreamingManager.formatSSE(chunk));
|
|
177
|
+
if (!ok && stream.isPaused === false) {
|
|
178
|
+
// Downstream can't keep up — will resume on drain from stream
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
stream.on('end', () => {
|
|
183
|
+
clearInterval(heartbeat);
|
|
184
|
+
res.end();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get activeCount(): number {
|
|
189
|
+
let count = 0;
|
|
190
|
+
for (const s of this.streams.values()) {
|
|
191
|
+
if (!s.isEnded) count++;
|
|
192
|
+
}
|
|
193
|
+
return count;
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/deploy/openclaw.ts
CHANGED
|
@@ -80,16 +80,6 @@ function generateAgentsMd(oad: OADDocument): string {
|
|
|
80
80
|
}
|
|
81
81
|
md += `\n`;
|
|
82
82
|
|
|
83
|
-
// DTV
|
|
84
|
-
if (dtv) {
|
|
85
|
-
md += `## Trust & Value\n\n`;
|
|
86
|
-
md += `- Trust Level: ${dtv.trust?.level ?? 'sandbox'}\n`;
|
|
87
|
-
if (dtv.value?.metrics?.length) {
|
|
88
|
-
md += `- Metrics: ${dtv.value.metrics.join(', ')}\n`;
|
|
89
|
-
}
|
|
90
|
-
md += `\n`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
83
|
return md;
|
|
94
84
|
}
|
|
95
85
|
|
|
@@ -198,3 +188,13 @@ export function deployToOpenClaw(options: DeployOptions): DeployResult {
|
|
|
198
188
|
|
|
199
189
|
return result;
|
|
200
190
|
}
|
|
191
|
+
:`, err);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.error(`Warning: OpenClaw config not found at ${configPath}`);
|
|
195
|
+
console.error(`Run 'openclaw init' first, then re-run with --install`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -52,8 +52,6 @@ export type { Locale } from './i18n';
|
|
|
52
52
|
export { KnowledgeBase } from './core/knowledge';
|
|
53
53
|
export { deployToHermes } from './deploy/hermes';
|
|
54
54
|
export type { HermesDeployOptions, HermesDeployResult } from './deploy/hermes';
|
|
55
|
-
export { publishAgent, installAgent } from './marketplace';
|
|
56
|
-
export type { AgentManifest, PublishOptions, InstallOptions } from './marketplace';
|
|
57
55
|
|
|
58
56
|
// v0.7.0 modules
|
|
59
57
|
export { createAuthMiddleware, getActiveSessions } from './core/auth';
|
|
@@ -108,3 +106,15 @@ export { DiscordChannel } from './channels/discord';
|
|
|
108
106
|
export type { DiscordChannelConfig } from './channels/discord';
|
|
109
107
|
export { ProcessWatcher } from './core/watch';
|
|
110
108
|
export type { WatchPattern, WatchMatch, WatchOptions } from './core/watch';
|
|
109
|
+
export { ProcessWatcher } from './core/watch';
|
|
110
|
+
export type { WatchPattern, WatchMatch, WatchOptions } from './core/watch';
|
|
111
|
+
|
|
112
|
+
// v1.2.0 modules
|
|
113
|
+
export { ToolGateway } from './tools/gateway';
|
|
114
|
+
export type { ToolGatewayConfig, GatewayToolName } from './tools/gateway';
|
|
115
|
+
export { StreamingManager, StreamableResponse } from './core/streaming';
|
|
116
|
+
export type { StreamChunk, StreamOptions } from './core/streaming';
|
|
117
|
+
|
|
118
|
+
// v1.3.0 modules
|
|
119
|
+
export { TraceCollector, ConsoleExporter, DeepBrainExporter } from './traces';
|
|
120
|
+
export type { Span, SpanEvent, TraceExporter } from './traces';
|
package/src/schema/oad.ts
CHANGED
|
@@ -151,5 +151,4 @@ export type SkillRef = z.infer<typeof SkillRefSchema>;
|
|
|
151
151
|
export type Channel = z.infer<typeof ChannelSchema>;
|
|
152
152
|
export type Metadata = z.infer<typeof MetadataSchema>;
|
|
153
153
|
export type Spec = z.infer<typeof SpecSchema>;
|
|
154
|
-
export type
|
|
155
|
-
export type TrustLevelType = z.infer<typeof TrustLevel>;
|
|
154
|
+
export type TrustLevelType = string;
|
|
@@ -25,10 +25,6 @@ export function createCodeReviewerConfig() {
|
|
|
25
25
|
],
|
|
26
26
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
27
27
|
memory: { shortTerm: true, longTerm: false },
|
|
28
|
-
dtv: {
|
|
29
|
-
trust: { level: 'sandbox' as const },
|
|
30
|
-
value: { metrics: ['reviews_completed', 'issues_found'] },
|
|
31
|
-
},
|
|
32
28
|
},
|
|
33
29
|
};
|
|
34
30
|
}
|
|
@@ -71,10 +71,6 @@ export function createCustomerServiceConfig() {
|
|
|
71
71
|
],
|
|
72
72
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
73
73
|
memory: { shortTerm: true, longTerm: false },
|
|
74
|
-
dtv: {
|
|
75
|
-
trust: { level: 'sandbox' as const },
|
|
76
|
-
value: { metrics: ['response_time', 'satisfaction_score'] },
|
|
77
|
-
},
|
|
78
74
|
},
|
|
79
75
|
};
|
|
80
76
|
}
|
|
@@ -61,10 +61,6 @@ export function createDataAnalystConfig() {
|
|
|
61
61
|
],
|
|
62
62
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
63
63
|
memory: { shortTerm: true, longTerm: true },
|
|
64
|
-
dtv: {
|
|
65
|
-
trust: { level: 'sandbox' as const },
|
|
66
|
-
value: { metrics: ['queries_processed', 'insights_generated'] },
|
|
67
|
-
},
|
|
68
64
|
},
|
|
69
65
|
};
|
|
70
66
|
}
|
|
@@ -22,10 +22,6 @@ export function createKnowledgeBaseConfig() {
|
|
|
22
22
|
],
|
|
23
23
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
24
24
|
memory: { shortTerm: true, longTerm: { provider: 'deepbrain' as const, collection: 'company-knowledge' } },
|
|
25
|
-
dtv: {
|
|
26
|
-
trust: { level: 'sandbox' as const },
|
|
27
|
-
value: { metrics: ['queries_answered', 'docs_indexed'] },
|
|
28
|
-
},
|
|
29
25
|
},
|
|
30
26
|
};
|
|
31
27
|
}
|
|
@@ -70,10 +70,6 @@ export function createSalesAssistantConfig() {
|
|
|
70
70
|
],
|
|
71
71
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
72
72
|
memory: { shortTerm: true, longTerm: false },
|
|
73
|
-
dtv: {
|
|
74
|
-
trust: { level: 'sandbox' as const },
|
|
75
|
-
value: { metrics: ['leads_captured', 'appointments_booked'] },
|
|
76
|
-
},
|
|
77
73
|
},
|
|
78
74
|
};
|
|
79
75
|
}
|
package/src/templates/teacher.ts
CHANGED
|
@@ -70,10 +70,6 @@ export function createTeacherConfig() {
|
|
|
70
70
|
],
|
|
71
71
|
channels: [{ type: 'web' as const, port: 3000 }],
|
|
72
72
|
memory: { shortTerm: true, longTerm: true },
|
|
73
|
-
dtv: {
|
|
74
|
-
trust: { level: 'sandbox' as const },
|
|
75
|
-
value: { metrics: ['lessons_created', 'quizzes_generated', 'concepts_explained'] },
|
|
76
|
-
},
|
|
77
73
|
},
|
|
78
74
|
};
|
|
79
75
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { AgentContext } from '../core/types';
|
|
2
|
+
import type { MCPTool, MCPToolDefinition, MCPToolResult } from './mcp';
|
|
3
|
+
|
|
4
|
+
// ─── Gateway Types ───────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface ToolGatewayConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
endpoint: string;
|
|
9
|
+
apiKey: string;
|
|
10
|
+
enabledTools?: GatewayToolName[];
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GatewayToolName = 'web-search' | 'image-gen' | 'tts' | 'browser';
|
|
15
|
+
|
|
16
|
+
interface GatewayToolMeta {
|
|
17
|
+
name: GatewayToolName;
|
|
18
|
+
description: string;
|
|
19
|
+
inputSchema: Record<string, unknown>;
|
|
20
|
+
available: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GatewayResponse {
|
|
24
|
+
content: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Gateway Tool Wrapper ────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
class GatewayTool implements MCPTool {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputSchema: Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private gateway: ToolGateway,
|
|
38
|
+
private meta: GatewayToolMeta,
|
|
39
|
+
) {
|
|
40
|
+
this.name = `gateway:${meta.name}`;
|
|
41
|
+
this.description = `[Gateway] ${meta.description}`;
|
|
42
|
+
this.inputSchema = meta.inputSchema;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execute(input: Record<string, unknown>, _context?: AgentContext): Promise<MCPToolResult> {
|
|
46
|
+
return this.gateway.invokeTool(this.meta.name, input);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Default Tool Definitions ────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const DEFAULT_TOOL_DEFS: GatewayToolMeta[] = [
|
|
53
|
+
{
|
|
54
|
+
name: 'web-search',
|
|
55
|
+
description: 'Search the web and return results',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
query: { type: 'string', description: 'Search query' },
|
|
60
|
+
count: { type: 'number', description: 'Number of results (1-10)' },
|
|
61
|
+
},
|
|
62
|
+
required: ['query'],
|
|
63
|
+
},
|
|
64
|
+
available: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'image-gen',
|
|
68
|
+
description: 'Generate images from text prompts',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
prompt: { type: 'string', description: 'Image generation prompt' },
|
|
73
|
+
size: { type: 'string', description: 'Image size (e.g. 1024x1024)' },
|
|
74
|
+
},
|
|
75
|
+
required: ['prompt'],
|
|
76
|
+
},
|
|
77
|
+
available: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'tts',
|
|
81
|
+
description: 'Convert text to speech audio',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
text: { type: 'string', description: 'Text to synthesize' },
|
|
86
|
+
voice: { type: 'string', description: 'Voice identifier' },
|
|
87
|
+
},
|
|
88
|
+
required: ['text'],
|
|
89
|
+
},
|
|
90
|
+
available: true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'browser',
|
|
94
|
+
description: 'Automated browser actions — navigate, screenshot, extract content',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
action: { type: 'string', description: 'Action: navigate | screenshot | extract' },
|
|
99
|
+
url: { type: 'string', description: 'Target URL' },
|
|
100
|
+
selector: { type: 'string', description: 'CSS selector for extraction' },
|
|
101
|
+
},
|
|
102
|
+
required: ['action', 'url'],
|
|
103
|
+
},
|
|
104
|
+
available: true,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// ─── ToolGateway ─────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export class ToolGateway {
|
|
111
|
+
private config: ToolGatewayConfig;
|
|
112
|
+
private availableTools: Map<GatewayToolName, GatewayToolMeta> = new Map();
|
|
113
|
+
private connected = false;
|
|
114
|
+
|
|
115
|
+
constructor(config: ToolGatewayConfig) {
|
|
116
|
+
this.config = {
|
|
117
|
+
timeout: 30_000,
|
|
118
|
+
...config,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Discover available tools from the gateway endpoint. */
|
|
123
|
+
async connect(): Promise<void> {
|
|
124
|
+
if (!this.config.enabled) return;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(`${this.config.endpoint}/tools`, {
|
|
128
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` },
|
|
129
|
+
signal: AbortSignal.timeout(this.config.timeout!),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(`Gateway returned ${res.status}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const data = (await res.json()) as { tools: GatewayToolMeta[] };
|
|
137
|
+
const enabledSet = this.config.enabledTools
|
|
138
|
+
? new Set(this.config.enabledTools)
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
for (const tool of data.tools) {
|
|
142
|
+
if (!enabledSet || enabledSet.has(tool.name)) {
|
|
143
|
+
this.availableTools.set(tool.name, tool);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.connected = true;
|
|
147
|
+
} catch {
|
|
148
|
+
// Auto-detect failed — fall back to default definitions
|
|
149
|
+
this.loadDefaults();
|
|
150
|
+
this.connected = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Load default tool definitions (used as fallback). */
|
|
155
|
+
private loadDefaults(): void {
|
|
156
|
+
const enabledSet = this.config.enabledTools
|
|
157
|
+
? new Set(this.config.enabledTools)
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
for (const def of DEFAULT_TOOL_DEFS) {
|
|
161
|
+
if (!enabledSet || enabledSet.has(def.name)) {
|
|
162
|
+
this.availableTools.set(def.name, def);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Invoke a tool through the gateway. */
|
|
168
|
+
async invokeTool(name: GatewayToolName, input: Record<string, unknown>): Promise<MCPToolResult> {
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(`${this.config.endpoint}/tools/${name}/invoke`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: {
|
|
173
|
+
'Content-Type': 'application/json',
|
|
174
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({ input }),
|
|
177
|
+
signal: AbortSignal.timeout(this.config.timeout!),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
return { content: `Gateway error: HTTP ${res.status}`, isError: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const data = (await res.json()) as GatewayResponse;
|
|
185
|
+
if (data.error) {
|
|
186
|
+
return { content: data.error, isError: true, metadata: data.metadata };
|
|
187
|
+
}
|
|
188
|
+
return { content: data.content, metadata: data.metadata };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
return {
|
|
191
|
+
content: `Gateway invocation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
192
|
+
isError: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Get all gateway tools as MCPTool instances for registry integration. */
|
|
198
|
+
getTools(): MCPTool[] {
|
|
199
|
+
return Array.from(this.availableTools.values()).map(
|
|
200
|
+
(meta) => new GatewayTool(this, meta),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Get tool definitions (without execute). */
|
|
205
|
+
listTools(): MCPToolDefinition[] {
|
|
206
|
+
return Array.from(this.availableTools.values()).map(({ name, description, inputSchema }) => ({
|
|
207
|
+
name: `gateway:${name}`,
|
|
208
|
+
description: `[Gateway] ${description}`,
|
|
209
|
+
inputSchema,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get isConnected(): boolean {
|
|
214
|
+
return this.connected;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get toolCount(): number {
|
|
218
|
+
return this.availableTools.size;
|
|
219
|
+
}
|
|
220
|
+
}
|