@tpsdev-ai/agent 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +32 -0
- package/dist/bin.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/governance/boundary.d.ts +30 -0
- package/dist/governance/boundary.d.ts.map +1 -0
- package/dist/governance/boundary.js +120 -0
- package/dist/governance/boundary.js.map +1 -0
- package/dist/governance/review-gate.d.ts +9 -0
- package/dist/governance/review-gate.d.ts.map +1 -0
- package/dist/governance/review-gate.js +28 -0
- package/dist/governance/review-gate.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/io/context.d.ts +18 -0
- package/dist/io/context.d.ts.map +1 -0
- package/dist/io/context.js +76 -0
- package/dist/io/context.js.map +1 -0
- package/dist/io/mail.d.ts +22 -0
- package/dist/io/mail.d.ts.map +1 -0
- package/dist/io/mail.js +45 -0
- package/dist/io/mail.js.map +1 -0
- package/dist/io/memory.d.ts +24 -0
- package/dist/io/memory.d.ts.map +1 -0
- package/dist/io/memory.js +91 -0
- package/dist/io/memory.js.map +1 -0
- package/dist/llm/provider.d.ts +26 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +254 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/runtime/agent.d.ts +14 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +46 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/event-loop.d.ts +32 -0
- package/dist/runtime/event-loop.d.ts.map +1 -0
- package/dist/runtime/event-loop.js +178 -0
- package/dist/runtime/event-loop.js.map +1 -0
- package/dist/runtime/types.d.ts +66 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/tools/edit.d.ts +4 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +46 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/exec.d.ts +4 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/exec.js +74 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mail.d.ts +4 -0
- package/dist/tools/mail.d.ts.map +1 -0
- package/dist/tools/mail.js +19 -0
- package/dist/tools/mail.js.map +1 -0
- package/dist/tools/read.d.ts +4 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +31 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +23 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/write.d.ts +4 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +32 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +19 -4
- package/src/bin.ts +40 -0
- package/src/config.ts +64 -0
- package/src/governance/boundary.ts +112 -18
- package/src/index.ts +15 -2
- package/src/io/context.ts +59 -15
- package/src/io/memory.ts +53 -9
- package/src/llm/provider.ts +203 -42
- package/src/runtime/agent.ts +30 -3
- package/src/runtime/event-loop.ts +168 -26
- package/src/runtime/types.ts +54 -6
- package/src/tools/edit.ts +59 -0
- package/src/tools/exec.ts +92 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/mail.ts +28 -0
- package/src/tools/read.ts +38 -0
- package/src/tools/registry.ts +16 -7
- package/src/tools/write.ts +40 -0
- package/src/types/js-tiktoken.d.ts +7 -0
- package/test/governance.test.ts +61 -32
- package/test/io.test.ts +15 -7
- package/test/runtime.test.ts +26 -5
package/src/llm/provider.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
LLMConfig,
|
|
3
|
+
CompletionRequest,
|
|
4
|
+
CompletionResponse,
|
|
5
|
+
ToolCall,
|
|
6
|
+
ToolSpec,
|
|
7
|
+
LLMMessage,
|
|
8
|
+
} from "../runtime/types.js";
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
systemPrompt?: string;
|
|
5
|
-
messages: Array<{ role: "user" | "assistant"; content: string }>;
|
|
6
|
-
maxTokens?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface CompletionResponse {
|
|
10
|
-
content: string;
|
|
11
|
-
inputTokens: number;
|
|
12
|
-
outputTokens: number;
|
|
13
|
-
}
|
|
10
|
+
type ProviderKind = LLMConfig["provider"];
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
* Routes completion requests to the
|
|
17
|
-
*
|
|
13
|
+
* Routes completion requests to the configured provider and normalizes
|
|
14
|
+
* tool-call responses into a common shape.
|
|
18
15
|
*/
|
|
19
16
|
export class ProviderManager {
|
|
20
17
|
constructor(private readonly config: LLMConfig) {}
|
|
@@ -25,6 +22,8 @@ export class ProviderManager {
|
|
|
25
22
|
return this.completeAnthropic(request);
|
|
26
23
|
case "google":
|
|
27
24
|
return this.completeGoogle(request);
|
|
25
|
+
case "openai":
|
|
26
|
+
return this.completeOpenAI(request);
|
|
28
27
|
case "ollama":
|
|
29
28
|
return this.completeOllama(request);
|
|
30
29
|
default:
|
|
@@ -32,10 +31,123 @@ export class ProviderManager {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
private toolSetForAnthropic(tools: ToolSpec[]) {
|
|
35
|
+
return tools.map((tool) => ({
|
|
36
|
+
name: tool.name,
|
|
37
|
+
description: tool.description,
|
|
38
|
+
input_schema: tool.input_schema,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private toolSetForOpenAI(tools: ToolSpec[]) {
|
|
43
|
+
return tools.map((tool) => ({
|
|
44
|
+
type: "function",
|
|
45
|
+
function: {
|
|
46
|
+
name: tool.name,
|
|
47
|
+
description: tool.description,
|
|
48
|
+
parameters: tool.input_schema,
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private toolSetForGoogle(tools: ToolSpec[]) {
|
|
54
|
+
return {
|
|
55
|
+
functionDeclarations: tools.map((tool) => ({
|
|
56
|
+
name: tool.name,
|
|
57
|
+
description: tool.description,
|
|
58
|
+
parameters: tool.input_schema,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private mapAnthropicResponse(raw: any): CompletionResponse {
|
|
64
|
+
const blocks = raw?.content ?? [];
|
|
65
|
+
const toolCalls: ToolCall[] = [];
|
|
66
|
+
let content = "";
|
|
67
|
+
|
|
68
|
+
for (const block of blocks) {
|
|
69
|
+
if (block?.type === "tool_use" && block?.name && block?.id) {
|
|
70
|
+
toolCalls.push({
|
|
71
|
+
id: block.id,
|
|
72
|
+
name: block.name,
|
|
73
|
+
input: typeof block.input === "object" ? block.input : {},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (block?.type === "text") {
|
|
77
|
+
content += block.text ?? "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
content,
|
|
83
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
84
|
+
inputTokens: raw?.usage?.input_tokens ?? 0,
|
|
85
|
+
outputTokens: raw?.usage?.output_tokens ?? 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private mapOpenAIResponse(raw: any): CompletionResponse {
|
|
90
|
+
const message = raw?.choices?.[0]?.message ?? {};
|
|
91
|
+
const toolCalls = (message.tool_calls ?? []).map((toolCall: any) => ({
|
|
92
|
+
id: toolCall?.id,
|
|
93
|
+
name: toolCall?.function?.name,
|
|
94
|
+
input: this.safeJson(toolCall?.function?.arguments),
|
|
95
|
+
})) as ToolCall[];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: message?.content ?? "",
|
|
99
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
100
|
+
inputTokens: raw?.usage?.prompt_tokens ?? 0,
|
|
101
|
+
outputTokens: raw?.usage?.completion_tokens ?? 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private mapOllamaResponse(raw: any): CompletionResponse {
|
|
106
|
+
const message = raw?.message ?? {};
|
|
107
|
+
let toolCalls: ToolCall[] | undefined;
|
|
108
|
+
|
|
109
|
+
if (message?.tool_calls) {
|
|
110
|
+
toolCalls = message.tool_calls.map((toolCall: any) => ({
|
|
111
|
+
id: toolCall?.id,
|
|
112
|
+
name: toolCall?.name,
|
|
113
|
+
input: this.safeJson(toolCall?.arguments),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: message?.content ?? "",
|
|
119
|
+
toolCalls: toolCalls && toolCalls.length ? toolCalls : undefined,
|
|
120
|
+
inputTokens: raw?.prompt_eval_count ?? 0,
|
|
121
|
+
outputTokens: raw?.eval_count ?? 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private safeJson(raw: unknown): Record<string, unknown> {
|
|
126
|
+
if (typeof raw === "object" && raw !== null) return raw as Record<string, unknown>;
|
|
127
|
+
if (typeof raw === "string") {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
35
138
|
private async completeAnthropic(request: CompletionRequest): Promise<CompletionResponse> {
|
|
36
139
|
const apiKey = this.config.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
37
140
|
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
|
|
38
141
|
|
|
142
|
+
const body = {
|
|
143
|
+
model: this.config.model,
|
|
144
|
+
max_tokens: request.maxTokens ?? 2048,
|
|
145
|
+
system: request.systemPrompt,
|
|
146
|
+
messages: request.messages,
|
|
147
|
+
tools: this.toolSetForAnthropic(request.tools),
|
|
148
|
+
tool_choice: request.toolChoice ?? "auto",
|
|
149
|
+
};
|
|
150
|
+
|
|
39
151
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
40
152
|
method: "POST",
|
|
41
153
|
headers: {
|
|
@@ -43,12 +155,7 @@ export class ProviderManager {
|
|
|
43
155
|
"x-api-key": apiKey,
|
|
44
156
|
"anthropic-version": "2023-06-01",
|
|
45
157
|
},
|
|
46
|
-
body: JSON.stringify(
|
|
47
|
-
model: this.config.model,
|
|
48
|
-
max_tokens: request.maxTokens ?? 2048,
|
|
49
|
-
system: request.systemPrompt,
|
|
50
|
-
messages: request.messages,
|
|
51
|
-
}),
|
|
158
|
+
body: JSON.stringify(body),
|
|
52
159
|
});
|
|
53
160
|
|
|
54
161
|
if (!res.ok) {
|
|
@@ -56,11 +163,7 @@ export class ProviderManager {
|
|
|
56
163
|
}
|
|
57
164
|
|
|
58
165
|
const data = (await res.json()) as any;
|
|
59
|
-
return
|
|
60
|
-
content: data.content?.[0]?.text ?? "",
|
|
61
|
-
inputTokens: data.usage?.input_tokens ?? 0,
|
|
62
|
-
outputTokens: data.usage?.output_tokens ?? 0,
|
|
63
|
-
};
|
|
166
|
+
return this.mapAnthropicResponse(data);
|
|
64
167
|
}
|
|
65
168
|
|
|
66
169
|
private async completeGoogle(request: CompletionRequest): Promise<CompletionResponse> {
|
|
@@ -68,17 +171,24 @@ export class ProviderManager {
|
|
|
68
171
|
if (!apiKey) throw new Error("GOOGLE_API_KEY not set");
|
|
69
172
|
|
|
70
173
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model}:generateContent?key=${apiKey}`;
|
|
174
|
+
const toolPayload = this.toolSetForGoogle(request.tools);
|
|
175
|
+
|
|
71
176
|
const res = await fetch(url, {
|
|
72
177
|
method: "POST",
|
|
73
178
|
headers: { "Content-Type": "application/json" },
|
|
74
179
|
body: JSON.stringify({
|
|
75
|
-
contents: request.messages.map((m) => ({
|
|
76
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
77
|
-
parts: [{ text: m.content }],
|
|
78
|
-
})),
|
|
79
180
|
systemInstruction: request.systemPrompt
|
|
80
181
|
? { parts: [{ text: request.systemPrompt }] }
|
|
81
182
|
: undefined,
|
|
183
|
+
contents: request.messages.map((m) => ({
|
|
184
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
185
|
+
parts: [{ text: m.content ?? "" }],
|
|
186
|
+
})),
|
|
187
|
+
tools: toolPayload,
|
|
188
|
+
toolConfig: request.toolChoice === "required" ? { functionCallingConfig: { mode: "ANY" } } : undefined,
|
|
189
|
+
generationConfig: {
|
|
190
|
+
maxOutputTokens: request.maxTokens ?? 2048,
|
|
191
|
+
},
|
|
82
192
|
}),
|
|
83
193
|
});
|
|
84
194
|
|
|
@@ -87,27 +197,64 @@ export class ProviderManager {
|
|
|
87
197
|
}
|
|
88
198
|
|
|
89
199
|
const data = (await res.json()) as any;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
200
|
+
const candidate = data?.candidates?.[0] ?? {};
|
|
201
|
+
const functionCalls = candidate?.content?.parts?.flatMap((part: any) => {
|
|
202
|
+
if (Array.isArray(part?.functionCalls)) return part.functionCalls;
|
|
203
|
+
if (part?.functionCall) return [part.functionCall];
|
|
204
|
+
return [];
|
|
205
|
+
}) ?? [];
|
|
206
|
+
|
|
207
|
+
const toolCalls = functionCalls.map((toolCall: any) => ({
|
|
208
|
+
id: toolCall?.id,
|
|
209
|
+
name: toolCall?.name,
|
|
210
|
+
input: this.safeJson(toolCall?.args),
|
|
211
|
+
} as ToolCall));
|
|
212
|
+
|
|
92
213
|
return {
|
|
93
|
-
content: text,
|
|
94
|
-
|
|
95
|
-
|
|
214
|
+
content: candidate?.content?.parts?.map((part: any) => part?.text).filter(Boolean).join("\n") ?? "",
|
|
215
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
216
|
+
inputTokens: data?.usageMetadata?.promptTokenCount ?? 0,
|
|
217
|
+
outputTokens: data?.usageMetadata?.candidatesTokenCount ?? 0,
|
|
96
218
|
};
|
|
97
219
|
}
|
|
98
220
|
|
|
221
|
+
private async completeOpenAI(request: CompletionRequest): Promise<CompletionResponse> {
|
|
222
|
+
const apiKey = this.config.apiKey ?? process.env.OPENAI_API_KEY;
|
|
223
|
+
if (!apiKey) throw new Error("OPENAI_API_KEY not set");
|
|
224
|
+
|
|
225
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
Authorization: `Bearer ${apiKey}`,
|
|
230
|
+
},
|
|
231
|
+
body: JSON.stringify({
|
|
232
|
+
model: this.config.model,
|
|
233
|
+
messages: request.messages,
|
|
234
|
+
tools: this.toolSetForOpenAI(request.tools),
|
|
235
|
+
tool_choice: request.toolChoice ?? "auto",
|
|
236
|
+
max_tokens: request.maxTokens ?? 2048,
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (!res.ok) {
|
|
241
|
+
throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const data = (await res.json()) as any;
|
|
245
|
+
return this.mapOpenAIResponse(data);
|
|
246
|
+
}
|
|
247
|
+
|
|
99
248
|
private async completeOllama(request: CompletionRequest): Promise<CompletionResponse> {
|
|
100
|
-
const baseUrl = this.config.baseUrl ?? "http://localhost:11434";
|
|
101
|
-
const systemMsg = request.systemPrompt
|
|
102
|
-
? [{ role: "system" as const, content: request.systemPrompt }]
|
|
103
|
-
: [];
|
|
249
|
+
const baseUrl = this.config.baseUrl ?? process.env.OLLAMA_HOST ?? "http://localhost:11434";
|
|
104
250
|
|
|
105
251
|
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
106
252
|
method: "POST",
|
|
107
253
|
headers: { "Content-Type": "application/json" },
|
|
108
254
|
body: JSON.stringify({
|
|
109
255
|
model: this.config.model,
|
|
110
|
-
messages:
|
|
256
|
+
messages: request.messages,
|
|
257
|
+
tools: this.toolSetForOpenAI(request.tools),
|
|
111
258
|
stream: false,
|
|
112
259
|
}),
|
|
113
260
|
});
|
|
@@ -117,10 +264,24 @@ export class ProviderManager {
|
|
|
117
264
|
}
|
|
118
265
|
|
|
119
266
|
const data = (await res.json()) as any;
|
|
267
|
+
return this.mapOllamaResponse(data);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public toToolSpec(name: string, description: string, inputSchema: Record<string, unknown>): ToolSpec {
|
|
120
271
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
272
|
+
name,
|
|
273
|
+
description,
|
|
274
|
+
input_schema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: inputSchema,
|
|
277
|
+
additionalProperties: false,
|
|
278
|
+
},
|
|
124
279
|
};
|
|
125
280
|
}
|
|
281
|
+
|
|
282
|
+
public toolInputSchemaFor(provider: ProviderKind): (schema: ToolSpec[]) => unknown {
|
|
283
|
+
if (provider === "openai" || provider === "ollama") return this.toolSetForOpenAI.bind(this);
|
|
284
|
+
if (provider === "google") return this.toolSetForGoogle.bind(this);
|
|
285
|
+
return this.toolSetForAnthropic.bind(this);
|
|
286
|
+
}
|
|
126
287
|
}
|
package/src/runtime/agent.ts
CHANGED
|
@@ -4,23 +4,50 @@ import { MailClient } from "../io/mail.js";
|
|
|
4
4
|
import { MemoryStore } from "../io/memory.js";
|
|
5
5
|
import { ContextManager } from "../io/context.js";
|
|
6
6
|
import { ProviderManager } from "../llm/provider.js";
|
|
7
|
+
import { BoundaryManager } from "../governance/boundary.js";
|
|
8
|
+
import { createDefaultToolset } from "../tools/index.js";
|
|
7
9
|
|
|
8
10
|
export class AgentRuntime {
|
|
9
11
|
private loop: EventLoop;
|
|
12
|
+
private readonly mail: MailClient;
|
|
13
|
+
private readonly boundary: BoundaryManager;
|
|
10
14
|
|
|
11
15
|
constructor(public readonly config: AgentConfig) {
|
|
12
16
|
const mail = new MailClient(config.mailDir);
|
|
13
17
|
const memory = new MemoryStore(config.memoryPath);
|
|
14
|
-
const context = new ContextManager(memory, config.contextWindowTokens ??
|
|
18
|
+
const context = new ContextManager(memory, config.contextWindowTokens ?? 8000);
|
|
15
19
|
const provider = new ProviderManager(config.llm);
|
|
16
|
-
|
|
20
|
+
|
|
21
|
+
this.mail = mail;
|
|
22
|
+
this.boundary = new BoundaryManager(config.workspace);
|
|
23
|
+
const tools = createDefaultToolset({
|
|
24
|
+
boundary: this.boundary,
|
|
25
|
+
mail,
|
|
26
|
+
tools: config.tools,
|
|
27
|
+
execAllowlist: config.execAllowlist,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.loop = new EventLoop({ config, memory, context, provider, tools });
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
async start(): Promise<void> {
|
|
20
|
-
|
|
34
|
+
const checkInbox = async () => this.mail.checkNewMail();
|
|
35
|
+
await this.loop.run(checkInbox);
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
async stop(): Promise<void> {
|
|
24
39
|
await this.loop.stop();
|
|
25
40
|
}
|
|
41
|
+
|
|
42
|
+
async runOnce(message: string): Promise<void> {
|
|
43
|
+
await this.loop.runOnce(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getState(): string {
|
|
47
|
+
return this.loop.getState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describeBoundaries(): string {
|
|
51
|
+
return this.boundary.describeCapabilities();
|
|
52
|
+
}
|
|
26
53
|
}
|
|
@@ -1,54 +1,60 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { MailMessage } from "../io/mail.js";
|
|
2
|
+
import type {
|
|
3
|
+
AgentConfig,
|
|
4
|
+
AgentState,
|
|
5
|
+
CompletionRequest,
|
|
6
|
+
CompletionResponse,
|
|
7
|
+
ToolCall,
|
|
8
|
+
ToolSpec,
|
|
9
|
+
} from "./types.js";
|
|
3
10
|
import type { MemoryStore } from "../io/memory.js";
|
|
4
11
|
import type { ContextManager } from "../io/context.js";
|
|
5
12
|
import type { ProviderManager } from "../llm/provider.js";
|
|
13
|
+
import type { ToolRegistry } from "../tools/registry.js";
|
|
14
|
+
import { ReviewGate } from "../governance/review-gate.js";
|
|
6
15
|
|
|
7
16
|
interface EventLoopDeps {
|
|
8
17
|
config: AgentConfig;
|
|
9
|
-
mail: MailClient;
|
|
10
18
|
memory: MemoryStore;
|
|
11
19
|
context: ContextManager;
|
|
12
20
|
provider: ProviderManager;
|
|
21
|
+
tools: ToolRegistry;
|
|
22
|
+
reviewGate?: ReviewGate;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sleep(ms: number): Promise<void> {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
export class EventLoop {
|
|
16
30
|
private state: AgentState = "idle";
|
|
17
31
|
private running = false;
|
|
18
32
|
|
|
19
|
-
constructor(private readonly deps: EventLoopDeps) {}
|
|
33
|
+
constructor(private readonly deps: EventLoopDeps, private readonly pollMs = 500) {}
|
|
20
34
|
|
|
21
|
-
async run(): Promise<void> {
|
|
35
|
+
async run(checkInbox: () => Promise<MailMessage[]>): Promise<void> {
|
|
22
36
|
this.running = true;
|
|
23
37
|
this.state = "idle";
|
|
24
38
|
|
|
25
39
|
while (this.running) {
|
|
26
|
-
const messages = await
|
|
27
|
-
|
|
28
|
-
if (messages.length === 0) {
|
|
29
|
-
// No work — sleep briefly before next poll
|
|
30
|
-
await sleep(500);
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
|
|
40
|
+
const messages = await checkInbox();
|
|
34
41
|
for (const msg of messages) {
|
|
35
42
|
if (!this.running) break;
|
|
36
|
-
|
|
37
43
|
this.state = "processing";
|
|
38
|
-
|
|
39
44
|
try {
|
|
40
|
-
await this.
|
|
41
|
-
} catch (err) {
|
|
42
|
-
// Log errors to memory and continue rather than crashing the loop
|
|
45
|
+
await this.processMail(msg);
|
|
46
|
+
} catch (err: any) {
|
|
43
47
|
await this.deps.memory.append({
|
|
44
48
|
type: "error",
|
|
45
49
|
ts: new Date().toISOString(),
|
|
46
|
-
data: String(err),
|
|
50
|
+
data: String(err?.message ?? err),
|
|
47
51
|
});
|
|
48
52
|
}
|
|
49
|
-
|
|
50
53
|
this.state = "idle";
|
|
51
54
|
}
|
|
55
|
+
|
|
56
|
+
if (!this.running) break;
|
|
57
|
+
if (messages.length === 0) await sleep(this.pollMs);
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -61,16 +67,152 @@ export class EventLoop {
|
|
|
61
67
|
return this.state;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
async runOnce(prompt: string): Promise<void> {
|
|
71
|
+
await this.processMessage(prompt);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async processMail(message: MailMessage): Promise<void> {
|
|
75
|
+
await this.processMessage(message.body);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async processMessage(promptRaw: string): Promise<void> {
|
|
79
|
+
const prompt = String(promptRaw).trim();
|
|
80
|
+
|
|
81
|
+
const tools = this.deps.tools
|
|
82
|
+
.list()
|
|
83
|
+
.map<ToolSpec>((tool) => ({
|
|
84
|
+
name: tool.name,
|
|
85
|
+
description: tool.description,
|
|
86
|
+
input_schema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: tool.input_schema,
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
66
93
|
await this.deps.memory.append({
|
|
67
94
|
type: "message",
|
|
68
95
|
ts: new Date().toISOString(),
|
|
69
|
-
data:
|
|
96
|
+
data: { direction: "in", body: prompt },
|
|
70
97
|
});
|
|
98
|
+
|
|
99
|
+
const request: CompletionRequest = {
|
|
100
|
+
systemPrompt: await this.buildSystemPrompt(),
|
|
101
|
+
messages: [{ role: "user", content: prompt }],
|
|
102
|
+
tools,
|
|
103
|
+
toolChoice: "auto",
|
|
104
|
+
maxTokens: this.deps.config.maxTokens ?? 1024,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let completion = await this.deps.provider.complete(request);
|
|
108
|
+
let turns = 0;
|
|
109
|
+
|
|
110
|
+
while (true) {
|
|
111
|
+
if (completion.content) {
|
|
112
|
+
await this.deps.memory.append({
|
|
113
|
+
type: "assistant",
|
|
114
|
+
ts: new Date().toISOString(),
|
|
115
|
+
data: {
|
|
116
|
+
content: completion.content,
|
|
117
|
+
inputTokens: completion.inputTokens,
|
|
118
|
+
outputTokens: completion.outputTokens,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!completion.toolCalls || completion.toolCalls.length === 0) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (turns++ > 8) {
|
|
128
|
+
await this.deps.memory.append({
|
|
129
|
+
type: "error",
|
|
130
|
+
ts: new Date().toISOString(),
|
|
131
|
+
data: { message: "tool loop max depth reached" },
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toolMessages: Array<{ role: "tool"; tool_call_id: string; name: string; content: string }> = [];
|
|
137
|
+
for (const call of completion.toolCalls ?? []) {
|
|
138
|
+
await this.deps.memory.append({
|
|
139
|
+
type: "tool_call",
|
|
140
|
+
ts: new Date().toISOString(),
|
|
141
|
+
data: { tool: call.name, args: call.input },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (this.deps.reviewGate && this.deps.reviewGate.isHighRisk(call.name)) {
|
|
145
|
+
await this.deps.reviewGate.requestApproval(call.name, call.input);
|
|
146
|
+
await this.deps.memory.append({
|
|
147
|
+
type: "approval_request",
|
|
148
|
+
ts: new Date().toISOString(),
|
|
149
|
+
data: { tool: call.name, args: call.input },
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let result;
|
|
155
|
+
try {
|
|
156
|
+
result = await this.deps.tools.execute(call.name, call.input);
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
result = { content: `Tool execution error: ${err?.message ?? String(err)}`, isError: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await this.deps.memory.append({
|
|
162
|
+
type: "tool_result",
|
|
163
|
+
ts: new Date().toISOString(),
|
|
164
|
+
data: { tool: call.name, result },
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
toolMessages.push({
|
|
168
|
+
role: "tool",
|
|
169
|
+
tool_call_id: String((call as ToolCall).id ?? `${Date.now()}-${Math.random()}`),
|
|
170
|
+
name: call.name,
|
|
171
|
+
content: JSON.stringify(result),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const nextMessages = [
|
|
176
|
+
...request.messages,
|
|
177
|
+
{ role: "assistant", content: completion.content ?? "" },
|
|
178
|
+
...toolMessages,
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
completion = await this.deps.provider.complete({
|
|
182
|
+
systemPrompt: request.systemPrompt,
|
|
183
|
+
messages: nextMessages,
|
|
184
|
+
tools,
|
|
185
|
+
toolChoice: "auto",
|
|
186
|
+
maxTokens: this.deps.config.maxTokens ?? 1024,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
71
189
|
}
|
|
72
|
-
}
|
|
73
190
|
|
|
74
|
-
|
|
75
|
-
|
|
191
|
+
private async buildSystemPrompt(): Promise<string> {
|
|
192
|
+
const fs = await import("node:fs/promises");
|
|
193
|
+
const docs = await Promise.all([
|
|
194
|
+
this.fileOrEmpty(`${this.deps.config.workspace}/SOUL.md`),
|
|
195
|
+
this.fileOrEmpty(`${this.deps.config.workspace}/AGENTS.md`),
|
|
196
|
+
this.fileOrEmpty(`${this.deps.config.workspace}/IDENTITY.md`),
|
|
197
|
+
Promise.resolve(this.deps.config.systemPrompt || ""),
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const sections = [
|
|
201
|
+
`Role: ${this.deps.config.name}`,
|
|
202
|
+
`Tools: ${this.deps.tools.list().map((t) => t.name).join(", ") || "(none)"}`,
|
|
203
|
+
`Context: ${this.deps.config.agentId}`,
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const docBlock = docs.filter(Boolean).join("\n\n");
|
|
207
|
+
return `${docBlock}\n\n${sections.join("\n")}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async fileOrEmpty(path: string): Promise<string> {
|
|
211
|
+
try {
|
|
212
|
+
const fs = await import("node:fs/promises");
|
|
213
|
+
return await fs.readFile(path, "utf-8");
|
|
214
|
+
} catch {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
76
218
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export type ProviderName = "anthropic" | "google" | "openai" | "ollama";
|
|
2
|
+
|
|
1
3
|
export interface LLMConfig {
|
|
2
|
-
provider:
|
|
4
|
+
provider: ProviderName;
|
|
3
5
|
model: string;
|
|
4
6
|
apiKey?: string;
|
|
5
7
|
baseUrl?: string;
|
|
@@ -10,19 +12,65 @@ export interface AgentConfig {
|
|
|
10
12
|
agentId: string;
|
|
11
13
|
/** Human-readable name */
|
|
12
14
|
name: string;
|
|
13
|
-
/** Maildir root
|
|
15
|
+
/** Maildir root */
|
|
14
16
|
mailDir: string;
|
|
15
17
|
/** JSONL memory file path */
|
|
16
18
|
memoryPath: string;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
+
/** Workspace root for file tools */
|
|
20
|
+
workspace: string;
|
|
19
21
|
/** LLM provider config */
|
|
20
22
|
llm: LLMConfig;
|
|
21
|
-
/**
|
|
23
|
+
/** Optional override personas/system prompt */
|
|
24
|
+
systemPrompt?: string;
|
|
25
|
+
/** Target token context window */
|
|
26
|
+
contextWindowTokens?: number;
|
|
27
|
+
/** Max model output tokens */
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
/** Tools the runtime should load */
|
|
30
|
+
tools?: Array<"read" | "write" | "edit" | "exec" | "mail">;
|
|
31
|
+
/** Allow-list for exec command binary names */
|
|
32
|
+
execAllowlist?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolResult {
|
|
36
|
+
content: string;
|
|
37
|
+
isError?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolCall {
|
|
41
|
+
id?: string;
|
|
42
|
+
name: string;
|
|
43
|
+
input: Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LLMMessage {
|
|
47
|
+
role: "system" | "user" | "assistant" | "tool" | "developer" | string;
|
|
48
|
+
content?: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
tool_call_id?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CompletionRequest {
|
|
22
54
|
systemPrompt?: string;
|
|
55
|
+
messages: LLMMessage[];
|
|
56
|
+
tools: ToolSpec[];
|
|
57
|
+
toolChoice?: "auto" | "required";
|
|
58
|
+
maxTokens?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ToolSpec {
|
|
62
|
+
name: string;
|
|
63
|
+
description: string;
|
|
64
|
+
input_schema: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CompletionResponse {
|
|
68
|
+
content?: string;
|
|
69
|
+
toolCalls?: ToolCall[];
|
|
70
|
+
inputTokens: number;
|
|
71
|
+
outputTokens: number;
|
|
23
72
|
}
|
|
24
73
|
|
|
25
|
-
/** Runtime state machine states */
|
|
26
74
|
export type AgentState =
|
|
27
75
|
| "idle"
|
|
28
76
|
| "processing"
|