@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.
Files changed (99) hide show
  1. package/README.md +54 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +32 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/config.d.ts +23 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +39 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/governance/boundary.d.ts +30 -0
  11. package/dist/governance/boundary.d.ts.map +1 -0
  12. package/dist/governance/boundary.js +120 -0
  13. package/dist/governance/boundary.js.map +1 -0
  14. package/dist/governance/review-gate.d.ts +9 -0
  15. package/dist/governance/review-gate.d.ts.map +1 -0
  16. package/dist/governance/review-gate.js +28 -0
  17. package/dist/governance/review-gate.js.map +1 -0
  18. package/dist/index.d.ts +16 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +19 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/io/context.d.ts +18 -0
  23. package/dist/io/context.d.ts.map +1 -0
  24. package/dist/io/context.js +76 -0
  25. package/dist/io/context.js.map +1 -0
  26. package/dist/io/mail.d.ts +22 -0
  27. package/dist/io/mail.d.ts.map +1 -0
  28. package/dist/io/mail.js +45 -0
  29. package/dist/io/mail.js.map +1 -0
  30. package/dist/io/memory.d.ts +24 -0
  31. package/dist/io/memory.d.ts.map +1 -0
  32. package/dist/io/memory.js +91 -0
  33. package/dist/io/memory.js.map +1 -0
  34. package/dist/llm/provider.d.ts +26 -0
  35. package/dist/llm/provider.d.ts.map +1 -0
  36. package/dist/llm/provider.js +254 -0
  37. package/dist/llm/provider.js.map +1 -0
  38. package/dist/runtime/agent.d.ts +14 -0
  39. package/dist/runtime/agent.d.ts.map +1 -0
  40. package/dist/runtime/agent.js +46 -0
  41. package/dist/runtime/agent.js.map +1 -0
  42. package/dist/runtime/event-loop.d.ts +32 -0
  43. package/dist/runtime/event-loop.d.ts.map +1 -0
  44. package/dist/runtime/event-loop.js +178 -0
  45. package/dist/runtime/event-loop.js.map +1 -0
  46. package/dist/runtime/types.d.ts +66 -0
  47. package/dist/runtime/types.d.ts.map +1 -0
  48. package/dist/runtime/types.js +2 -0
  49. package/dist/runtime/types.js.map +1 -0
  50. package/dist/tools/edit.d.ts +4 -0
  51. package/dist/tools/edit.d.ts.map +1 -0
  52. package/dist/tools/edit.js +46 -0
  53. package/dist/tools/edit.js.map +1 -0
  54. package/dist/tools/exec.d.ts +4 -0
  55. package/dist/tools/exec.d.ts.map +1 -0
  56. package/dist/tools/exec.js +74 -0
  57. package/dist/tools/exec.js.map +1 -0
  58. package/dist/tools/index.d.ts +17 -0
  59. package/dist/tools/index.d.ts.map +1 -0
  60. package/dist/tools/index.js +23 -0
  61. package/dist/tools/index.js.map +1 -0
  62. package/dist/tools/mail.d.ts +4 -0
  63. package/dist/tools/mail.d.ts.map +1 -0
  64. package/dist/tools/mail.js +19 -0
  65. package/dist/tools/mail.js.map +1 -0
  66. package/dist/tools/read.d.ts +4 -0
  67. package/dist/tools/read.d.ts.map +1 -0
  68. package/dist/tools/read.js +31 -0
  69. package/dist/tools/read.js.map +1 -0
  70. package/dist/tools/registry.d.ts +24 -0
  71. package/dist/tools/registry.d.ts.map +1 -0
  72. package/dist/tools/registry.js +23 -0
  73. package/dist/tools/registry.js.map +1 -0
  74. package/dist/tools/write.d.ts +4 -0
  75. package/dist/tools/write.d.ts.map +1 -0
  76. package/dist/tools/write.js +32 -0
  77. package/dist/tools/write.js.map +1 -0
  78. package/package.json +19 -4
  79. package/src/bin.ts +40 -0
  80. package/src/config.ts +64 -0
  81. package/src/governance/boundary.ts +112 -18
  82. package/src/index.ts +15 -2
  83. package/src/io/context.ts +59 -15
  84. package/src/io/memory.ts +53 -9
  85. package/src/llm/provider.ts +203 -42
  86. package/src/runtime/agent.ts +30 -3
  87. package/src/runtime/event-loop.ts +168 -26
  88. package/src/runtime/types.ts +54 -6
  89. package/src/tools/edit.ts +59 -0
  90. package/src/tools/exec.ts +92 -0
  91. package/src/tools/index.ts +30 -0
  92. package/src/tools/mail.ts +28 -0
  93. package/src/tools/read.ts +38 -0
  94. package/src/tools/registry.ts +16 -7
  95. package/src/tools/write.ts +40 -0
  96. package/src/types/js-tiktoken.d.ts +7 -0
  97. package/test/governance.test.ts +61 -32
  98. package/test/io.test.ts +15 -7
  99. package/test/runtime.test.ts +26 -5
@@ -1,20 +1,17 @@
1
- import type { LLMConfig } from "../runtime/types.js";
1
+ import type {
2
+ LLMConfig,
3
+ CompletionRequest,
4
+ CompletionResponse,
5
+ ToolCall,
6
+ ToolSpec,
7
+ LLMMessage,
8
+ } from "../runtime/types.js";
2
9
 
3
- export interface CompletionRequest {
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 appropriate provider.
17
- * Supports Anthropic, Google, and local Ollama.
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 text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
91
- const usage = data.usageMetadata ?? {};
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
- inputTokens: usage.promptTokenCount ?? 0,
95
- outputTokens: usage.candidatesTokenCount ?? 0,
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: [...systemMsg, ...request.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
- content: data.message?.content ?? "",
122
- inputTokens: data.prompt_eval_count ?? 0,
123
- outputTokens: data.eval_count ?? 0,
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
  }
@@ -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 ?? 8_000);
18
+ const context = new ContextManager(memory, config.contextWindowTokens ?? 8000);
15
19
  const provider = new ProviderManager(config.llm);
16
- this.loop = new EventLoop({ config, mail, memory, context, provider });
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
- await this.loop.run();
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 { AgentConfig, AgentState } from "./types.js";
2
- import type { MailClient } from "../io/mail.js";
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 this.deps.mail.checkNewMail();
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.processMessage(msg);
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
- private async processMessage(msg: unknown): Promise<void> {
65
- // Stub: real implementation builds prompt, calls LLM, handles tool calls
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: msg,
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
- function sleep(ms: number): Promise<void> {
75
- return new Promise((resolve) => setTimeout(resolve, ms));
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
  }
@@ -1,5 +1,7 @@
1
+ export type ProviderName = "anthropic" | "google" | "openai" | "ollama";
2
+
1
3
  export interface LLMConfig {
2
- provider: "anthropic" | "google" | "ollama";
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: ~/mail/inbox, ~/mail/outbox */
15
+ /** Maildir root */
14
16
  mailDir: string;
15
17
  /** JSONL memory file path */
16
18
  memoryPath: string;
17
- /** Target token context window (for compaction) */
18
- contextWindowTokens?: number;
19
+ /** Workspace root for file tools */
20
+ workspace: string;
19
21
  /** LLM provider config */
20
22
  llm: LLMConfig;
21
- /** System prompt override */
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"