Skip to content

Commit a2e6ae2

Browse files
committed
Add features: implement dynamic system prompt generation, autofix for diff edits, explicit context window management, transport layer abstraction, interactive main menu, and token usage tracking
1 parent cd1b103 commit a2e6ae2

File tree

7 files changed

+189
-76
lines changed

7 files changed

+189
-76
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ help: ## Show this help message
3737
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
3838

3939
install: ## Install project dependencies
40-
$(PACKAGE_MANAGER) install
40+
$(PACKAGE_MANAGER) install --legacy-peer-deps
4141

4242
build: check-deps ## Build the project for production
4343
$(PACKAGE_MANAGER) run build

docs/AVANCED-FEATURES.md

Lines changed: 68 additions & 68 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"simple-git": "^3.28.0",
4343
"winston": "^3.17.0",
4444
"zod": "^4.0.17",
45-
"zustand": "^5.0.7"
45+
"zustand": "^5.0.7",
46+
"typescript": "^5.5.3",
47+
"zod-to-ts": "^1.2.0"
4648
},
4749
"devDependencies": {
4850
"@types/node": "^20.14.9",
@@ -58,7 +60,6 @@
5860
"prettier": "^3.3.2",
5961
"tsc-alias": "^1.8.10",
6062
"tsx": "^4.16.2",
61-
"typescript": "^5.5.3",
6263
"vite-tsconfig-paths": "^4.3.2",
6364
"vitest": "^3.2.4"
6465
},

src/agent/llm.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ export function createLlmProvider(modelConfig: ModelConfig, config: Config): Lan
4343
}
4444
}
4545

46-
export async function streamAssistantResponse(history: ModelMessage[], config: Config) {
47-
// ... (this part remains the same)
46+
export async function streamAssistantResponse(
47+
history: ModelMessage[],
48+
config: Config,
49+
systemPrompt: string,
50+
) {
4851
const modelConfig = config.models.find((m) => m.name === config.defaultModel);
4952
if (!modelConfig) {
5053
throw new FatalError(`Default model "${config.defaultModel}" not found in configuration.`);
@@ -55,12 +58,12 @@ export async function streamAssistantResponse(history: ModelMessage[], config: C
5558
const truncatedHistory =
5659
maxItems && history.length > maxItems ? history.slice(-maxItems) : history;
5760
logger.info("Streaming text from LLM provider.");
58-
logger.debug({ systemPrompt: config.systemPrompt, messages: truncatedHistory });
61+
logger.debug({ systemPrompt, messages: truncatedHistory });
5962

6063
try {
6164
return await streamText({
6265
model: llmProvider,
63-
system: config.systemPrompt,
66+
system: systemPrompt,
6467
messages: truncatedHistory,
6568
experimental_telemetry: { isEnabled: false },
6669
tools: Object.fromEntries(

src/agent/state.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { create } from "zustand";
55
import logger from "@/logger.js";
66
import { streamAssistantResponse } from "./llm.js";
7+
import { generateSystemPrompt } from "./system-prompt.js";
78
import { runTool } from "./tools/index.js";
89
import { type Config, HISTORY_PATH, loadConfig, saveConfig } from "@/config.js";
910
import { FatalError, TransientError } from "./errors.js";
@@ -297,8 +298,10 @@ export const useStore = create<AppState & AppActions>((set, get) => ({
297298
})
298299
.filter(Boolean) as ModelMessage[];
299300

301+
const systemPrompt = await generateSystemPrompt(config);
302+
300303
const { textStream, toolCalls: toolCallPartsPromise } =
301-
await streamAssistantResponse(sdkCompliantHistory, config);
304+
await streamAssistantResponse(sdkCompliantHistory, config, systemPrompt);
302305

303306
let assistantMessage: HistoryItem | null = null;
304307
for await (const part of textStream) {

src/agent/system-prompt.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// src/agent/system-prompt.ts
2+
import { type Config } from "@/config.js";
3+
import { toolModules } from "./tools/definitions/index.js";
4+
import { zodToTs, printNode } from "zod-to-ts";
5+
import fs from "fs/promises";
6+
import path from "path";
7+
import os from "os";
8+
9+
/**
10+
* Recursively searches for instruction files (TOBI.md, AGENTS.md) upwards from the current directory.
11+
* @param currentDir The directory to start searching from.
12+
* @returns The content of the found file, or null if no file is found.
13+
*/
14+
async function findInstructionFile(currentDir: string): Promise<string | null> {
15+
const instructionFiles = ["TOBI.md", "AGENTS.md"];
16+
const homeDir = os.homedir();
17+
18+
let dir = currentDir;
19+
// Stop if we reach the root directory or the user's home directory
20+
while (dir !== path.dirname(dir) && dir.startsWith(homeDir)) {
21+
for (const fileName of instructionFiles) {
22+
const filePath = path.join(dir, fileName);
23+
try {
24+
// Check if the file exists and we can read it
25+
await fs.access(filePath, fs.constants.R_OK);
26+
return await fs.readFile(filePath, "utf-8");
27+
} catch {
28+
// File does not exist or is not readable, continue searching
29+
}
30+
}
31+
// Move to the parent directory
32+
dir = path.dirname(dir);
33+
}
34+
return null;
35+
}
36+
37+
/**
38+
* Generates the dynamic system prompt for the agent.
39+
* @param config The application configuration.
40+
* @returns A promise that resolves to the generated system prompt string.
41+
*/
42+
export async function generateSystemPrompt(_config: Config): Promise<string> {
43+
const cwd = process.cwd();
44+
45+
// 1. Get workspace files
46+
const dirents = await fs.readdir(cwd, { withFileTypes: true });
47+
const filesAndDirs = dirents.map((d) => (d.isDirectory() ? `${d.name}/` : d.name)).join("\n");
48+
49+
// 2. Find instruction file
50+
const instructionContent = await findInstructionFile(cwd);
51+
52+
// 3. Generate tool definitions as TypeScript types
53+
const toolDefinitions = Object.values(toolModules)
54+
.map((module) => {
55+
// We pass the zod schema for the arguments to zodToTs
56+
const { node } = zodToTs(module.schema.shape.arguments);
57+
// Then we print the resulting TypeScript AST node to a string.
58+
const argumentsString = printNode(node);
59+
60+
return (
61+
`// ${module.description}\n` +
62+
`type ${module.schema.shape.name.value} = {\n` +
63+
` name: "${module.schema.shape.name.value}";\n` +
64+
` arguments: ${argumentsString};\n` +
65+
`};`
66+
);
67+
})
68+
.join("\n\n");
69+
70+
// 4. Assemble the prompt
71+
const promptParts: string[] = [
72+
`You are a helpful AI assistant named Tobi. You can use tools to help the user with coding and file system tasks.`,
73+
];
74+
75+
promptParts.push("### Current Workspace");
76+
promptParts.push("Here is a list of files and directories in the current working directory:");
77+
promptParts.push("```");
78+
promptParts.push(filesAndDirs);
79+
promptParts.push("```");
80+
81+
if (instructionContent) {
82+
promptParts.push("### User-Provided Instructions");
83+
promptParts.push(
84+
"The user has provided the following instructions in a `TOBI.md` or `AGENTS.md` file. Follow them carefully.",
85+
);
86+
promptParts.push("```markdown");
87+
promptParts.push(instructionContent);
88+
promptParts.push("```");
89+
}
90+
91+
promptParts.push("### Available Tools");
92+
promptParts.push(
93+
"You have the following tools available. To use a tool, respond with a JSON object that strictly adheres to the TypeScript type definition of the tool.",
94+
);
95+
promptParts.push(
96+
"The following are TypeScript type definitions for the tools. The `name` property is the tool to call, and you must provide the corresponding `arguments` object.",
97+
);
98+
promptParts.push("```typescript");
99+
promptParts.push(toolDefinitions);
100+
promptParts.push("```");
101+
102+
return promptParts.join("\n\n");
103+
}

tests/agent/state.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { FatalError, TransientError } from "@/agent/errors";
55
// Mock dependencies
66
vi.mock("@/agent/llm");
77
vi.mock("@/agent/tools");
8+
vi.mock("@/agent/system-prompt", () => ({
9+
generateSystemPrompt: vi.fn().mockResolvedValue("mocked system prompt"),
10+
}));
811
vi.mock("@/config", () => ({
912
...vi.importActual("@/config"),
1013
getConfigDir: () => "/tmp/tobi-test",

0 commit comments

Comments
 (0)