aiko 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4d6bb00af4ebfc5d2758c6c65bf67f79d1676f6f1d84ca6c71bd926afa05a5ee
4
+ data.tar.gz: 27fd6ead4d6c6bb6e315188be9a2fa1cddbad6153155d27762a453b815d04d4f
5
+ SHA512:
6
+ metadata.gz: 547a3023dd19bfc0b6f2491d5fc0705f2f4dfd6b6747221a13ca6c2124f43169735e4b96c4dc8d606cf8092f11393ec3ff3a31949caa6ac903e842df1359ad03
7
+ data.tar.gz: 6e7219dec3d1a0b0d061527a726845b5de4d7ae5db77016b76d31e97c77da5cdc1367b5b6b1937701f8b97e511c4eb448bb77e8923bf082f548e5785c8687ac6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Satoshi Takei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.ja.md ADDED
@@ -0,0 +1,68 @@
1
+ # aiko
2
+
3
+ [English README](README.md)
4
+
5
+ AI + coding. OpenAI互換LLM API(DeepSeek / OpenRouter等)で動くRuby製CLIコーディングエージェント。
6
+ LLMにファイル操作・コマンド実行・検索のツールを与え、エージェントループで自律的にコーディングタスクを実行します。
7
+
8
+ > 名前の由来は「AI + コーディング」。「コーディング」の「コ」を取って、AI + ko = aiko です。
9
+
10
+ ## インストール
11
+
12
+ ```sh
13
+ bundle install
14
+ rake install # gemをビルドしてローカルインストール(aikoコマンドが使えるようになる)
15
+ ```
16
+
17
+ ## 設定
18
+
19
+ APIキーは環境変数で設定します(設定ファイルには書けません)。
20
+
21
+ ```sh
22
+ # DeepSeek本家(デフォルト。deepseek-v4-flashを使用)
23
+ export AIKO_API_KEY=sk-...
24
+
25
+ # OpenRouterを使う場合(例: より高性能なdeepseek-v4-pro)
26
+ export AIKO_API_KEY=sk-or-...
27
+ export AIKO_BASE_URL=https://openrouter.ai/api/v1
28
+ export AIKO_MODEL=deepseek/deepseek-v4-pro
29
+ ```
30
+
31
+ 任意で `~/.aiko/config.json` に `base_url` / `model` / `max_iterations` を設定できます。
32
+ 優先順位は「コマンドラインオプション > 環境変数 > 設定ファイル > デフォルト」です。
33
+
34
+ ## 使い方
35
+
36
+ ```sh
37
+ # ワンショット: プロンプトを1回処理して終了
38
+ aiko "fizzbuzz.rb を作成し、実行して動作確認してください"
39
+
40
+ # REPL: 対話モード(/help でコマンド一覧、/exit で終了)
41
+ aiko
42
+
43
+ # 主要オプション
44
+ aiko -y "..." # すべてのツール実行を自動承認
45
+ aiko -w DIR "..." # 作業ディレクトリを指定
46
+ aiko -m MODEL "..." # モデル名を上書き
47
+ aiko --max-iterations 30 # エージェントループの上限回数
48
+ ```
49
+
50
+ ファイル書き込み・編集・コマンド実行の前にはユーザー承認を求めます。
51
+
52
+ > **警告**: `-y` / `--yes` はLLMが提案したファイル書き込みやシェルコマンドを確認なしで実行します。
53
+ > 内容を確認できない操作が無条件に実行されるため、利用は自己責任で、信頼できる作業ディレクトリでのみ使用してください。
54
+
55
+ ## 開発
56
+
57
+ ```sh
58
+ rake test # テスト実行(デフォルトタスク)
59
+ rake run -- -y "..." # インストールせずに動作確認
60
+ ```
61
+
62
+ 仕様は [doc/spec/](doc/spec/01_overview.md)、実装手順は [doc/implementation/](doc/implementation/00_README.md) を参照。
63
+
64
+ ## 作者
65
+
66
+ Satoshi Takei — [GitHub @takeisa](https://github.com/takeisa) / [X @takeisa](https://x.com/takeisa)
67
+
68
+ AIコーディングエージェントを利用して開発しています。設計・実装の記録は [doc/](doc/spec/01_overview.md) を参照。
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # aiko
2
+
3
+ [日本語版 README](README.ja.md)
4
+
5
+ AI + coding. A Ruby CLI coding agent that runs on OpenAI-compatible LLM APIs (DeepSeek, OpenRouter, etc.).
6
+ It gives the LLM tools for file operations, command execution, and search, and carries out coding tasks autonomously through an agent loop.
7
+
8
+ > The name comes from "AI + コーディング (coding)" — taking "コ" (ko), the first syllable of コーディング, gives AI + ko = aiko.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ bundle install
14
+ rake install # build the gem and install it locally (makes the `aiko` command available)
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ The API key is set via an environment variable (it cannot be set in a config file).
20
+
21
+ ```sh
22
+ # DeepSeek (default; uses deepseek-v4-flash)
23
+ export AIKO_API_KEY=sk-...
24
+
25
+ # Using OpenRouter (e.g. the more capable deepseek-v4-pro)
26
+ export AIKO_API_KEY=sk-or-...
27
+ export AIKO_BASE_URL=https://openrouter.ai/api/v1
28
+ export AIKO_MODEL=deepseek/deepseek-v4-pro
29
+ ```
30
+
31
+ Optionally, you can set `base_url` / `model` / `max_iterations` in `~/.aiko/config.json`.
32
+ The precedence is: command-line options > environment variables > config file > defaults.
33
+
34
+ ## Usage
35
+
36
+ ```sh
37
+ # One-shot: process a single prompt and exit
38
+ aiko "Create fizzbuzz.rb, run it, and verify it works"
39
+
40
+ # REPL: interactive mode (/help for command list, /exit to quit)
41
+ aiko
42
+
43
+ # Main options
44
+ aiko -y "..." # auto-approve all tool executions
45
+ aiko -w DIR "..." # specify working directory
46
+ aiko -m MODEL "..." # override the model name
47
+ aiko --max-iterations 30 # cap the number of agent loop iterations
48
+ ```
49
+
50
+ aiko asks for your approval before writing/editing files or running commands.
51
+
52
+ > **Warning**: `-y` / `--yes` runs file writes/edits and shell commands proposed by the LLM
53
+ > without confirmation. Since operations you cannot review will run unconditionally,
54
+ > use this at your own risk, and only in a directory you trust.
55
+
56
+ ## Development
57
+
58
+ ```sh
59
+ rake test # run tests (default task)
60
+ rake run -- -y "..." # try it out without installing
61
+ ```
62
+
63
+ See [doc/spec/](doc/spec/01_overview.md) for the specification and [doc/implementation/](doc/implementation/00_README.md) for the implementation notes.
64
+
65
+ ## Author
66
+
67
+ Satoshi Takei — [GitHub @takeisa](https://github.com/takeisa) / [X @takeisa](https://x.com/takeisa)
68
+
69
+ Developed using an AI coding agent. See [doc/](doc/spec/01_overview.md) for the design and implementation notes.
data/exe/aiko ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "aiko"
5
+ exit Aiko::CLI.start(ARGV)
data/lib/aiko/agent.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ class Agent
5
+ MAX_ITERATIONS_MESSAGE = "(最大ループ回数に達したため処理を打ち切りました)"
6
+
7
+ attr_reader :conversation
8
+
9
+ def initialize(llm:, registry:, callbacks:, conversation:, max_iterations: 20)
10
+ @llm = llm
11
+ @registry = registry
12
+ @callbacks = callbacks
13
+ @conversation = conversation
14
+ @max_iterations = max_iterations
15
+ end
16
+
17
+ def run(user_input)
18
+ @conversation.add_user(user_input)
19
+ @max_iterations.times do
20
+ response = request_completion
21
+ return final_answer(response) unless response.tool_calls?
22
+
23
+ report_progress(response)
24
+ execute_tool_calls(response.tool_calls)
25
+ end
26
+ @callbacks.on_assistant_text(MAX_ITERATIONS_MESSAGE)
27
+ MAX_ITERATIONS_MESSAGE
28
+ end
29
+
30
+ private
31
+
32
+ def request_completion
33
+ @callbacks.on_request_start
34
+ response = begin
35
+ @llm.chat(messages: @conversation.messages, tools: @registry.schemas)
36
+ ensure
37
+ @callbacks.on_request_end
38
+ end
39
+ @conversation.add_assistant(response.raw_message)
40
+ response
41
+ end
42
+
43
+ def final_answer(response)
44
+ @callbacks.on_assistant_text(response.content)
45
+ response.content
46
+ end
47
+
48
+ def report_progress(response)
49
+ @callbacks.on_assistant_text(response.content) unless response.content.empty?
50
+ end
51
+
52
+ def execute_tool_calls(tool_calls)
53
+ tool_calls.each do |tool_call|
54
+ @callbacks.on_tool_start(tool_call.name, tool_call.arguments)
55
+ result = execute_tool_call(tool_call)
56
+ @callbacks.on_tool_result(tool_call.name, result)
57
+ @conversation.add_tool_result(tool_call_id: tool_call.id, content: result)
58
+ end
59
+ end
60
+
61
+ def execute_tool_call(tool_call)
62
+ if tool_call.parse_error
63
+ "Error: invalid tool arguments JSON: #{tool_call.parse_error}"
64
+ else
65
+ @registry.dispatch(tool_call.name, tool_call.arguments, callbacks: @callbacks)
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/aiko/cli.rb ADDED
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Aiko
6
+ class CLI
7
+ REPL_HELP = <<~HELP
8
+ /exit, /quit 終了 (Ctrl-D も同様)
9
+ /clear 会話履歴をクリア
10
+ /help このヘルプを表示
11
+ HELP
12
+
13
+ def self.start(argv)
14
+ new.start(argv)
15
+ end
16
+
17
+ def self.parse(argv)
18
+ options = {}
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = "Usage: aiko [options] [prompt]"
21
+ opts.on("-w", "--workdir DIR", "Working directory (default: current directory)") { |v| options[:workdir] = v }
22
+ opts.on("-m", "--model MODEL", "Override model name") { |v| options[:model] = v }
23
+ opts.on("--base-url URL", "Override API endpoint") { |v| options[:base_url] = v }
24
+ opts.on("-y", "--yes", "Auto-approve all tool executions") { options[:yes] = true }
25
+ opts.on("--max-iterations N", Integer, "Max agent loop iterations (default: 20)") { |v| options[:max_iterations] = v }
26
+ opts.on("-v", "--version", "Show version") { options[:version] = true }
27
+ opts.on("-h", "--help", "Show help") { options[:help] = true }
28
+ end
29
+ rest = parser.parse(argv)
30
+ options[:help_text] = parser.to_s
31
+ prompt = rest.empty? ? nil : rest.join(" ")
32
+ [options, prompt]
33
+ end
34
+
35
+ def start(argv)
36
+ options, prompt = self.class.parse(argv)
37
+ return show_version if options[:version]
38
+ return show_help(options) if options[:help]
39
+
40
+ config = Config.load(config_options(options))
41
+ ui = UI.new(auto_approve: options.fetch(:yes, false))
42
+ agent = build_agent(config, ui)
43
+ prompt ? run_oneshot(agent, ui, prompt) : run_repl(agent, ui, config)
44
+ rescue OptionParser::ParseError, ConfigError => e
45
+ warn "Error: #{e.message}"
46
+ 2
47
+ end
48
+
49
+ private
50
+
51
+ def config_options(options)
52
+ options.slice(:workdir, :model, :base_url, :max_iterations)
53
+ end
54
+
55
+ def show_version
56
+ puts "aiko #{VERSION}"
57
+ 0
58
+ end
59
+
60
+ def show_help(options)
61
+ puts options[:help_text]
62
+ 0
63
+ end
64
+
65
+ def build_agent(config, ui)
66
+ registry = Tools::Registry.new
67
+ [Tools::ReadFile, Tools::WriteFile, Tools::EditFile,
68
+ Tools::ListFiles, Tools::RunCommand, Tools::Search].each do |klass|
69
+ registry.register(klass.new(workdir: config.workdir))
70
+ end
71
+ llm = LLM::OpenAICompatible.new(
72
+ api_key: config.api_key, base_url: config.base_url, model: config.model
73
+ )
74
+ conversation = Conversation.new(system_prompt: SystemPrompt.build(workdir: config.workdir))
75
+ Agent.new(llm: llm, registry: registry, callbacks: ui,
76
+ conversation: conversation, max_iterations: config.max_iterations)
77
+ end
78
+
79
+ def run_oneshot(agent, ui, prompt)
80
+ agent.run(prompt)
81
+ 0
82
+ rescue LLM::APIError => e
83
+ ui.show_error(e.message)
84
+ 1
85
+ rescue Interrupt
86
+ 130
87
+ end
88
+
89
+ def run_repl(agent, ui, config)
90
+ require "reline"
91
+ ui.puts "aiko #{VERSION} | model: #{config.model} | workdir: #{config.workdir}"
92
+ ui.puts "Type /help for commands, /exit to quit."
93
+ loop do
94
+ line = read_line
95
+ break if line.nil?
96
+
97
+ line = line.strip
98
+ next if line.empty?
99
+
100
+ if line.start_with?("/")
101
+ break if %w[/exit /quit].include?(line)
102
+
103
+ handle_repl_command(agent, ui, line)
104
+ else
105
+ run_repl_turn(agent, ui, line)
106
+ end
107
+ end
108
+ 0
109
+ end
110
+
111
+ def read_line
112
+ Reline.readline("> ", true)
113
+ rescue Interrupt
114
+ ""
115
+ end
116
+
117
+ def handle_repl_command(agent, ui, line)
118
+ case line
119
+ when "/clear"
120
+ agent.conversation.clear
121
+ ui.puts "(会話履歴をクリアしました)"
122
+ when "/help"
123
+ ui.puts REPL_HELP
124
+ else
125
+ ui.show_error("unknown command: #{line} (/help で一覧を表示)")
126
+ end
127
+ end
128
+
129
+ def run_repl_turn(agent, ui, line)
130
+ agent.run(line)
131
+ rescue Interrupt
132
+ ui.puts "(中断しました)"
133
+ rescue LLM::APIError => e
134
+ ui.show_error(e.message)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Aiko
6
+ class Config
7
+ DEFAULTS = {
8
+ base_url: "https://api.deepseek.com",
9
+ model: "deepseek-v4-flash",
10
+ max_iterations: 20
11
+ }.freeze
12
+
13
+ FILE_KEYS = %w[base_url model max_iterations].freeze
14
+
15
+ attr_reader :api_key, :base_url, :model, :max_iterations, :workdir
16
+
17
+ def self.load(cli_options = {}, config_path: default_config_path, env: ENV)
18
+ file = load_file(config_path)
19
+ merged = DEFAULTS.dup
20
+ FILE_KEYS.each do |key|
21
+ merged[key.to_sym] = file[key] if file.key?(key)
22
+ end
23
+ merged[:base_url] = env["AIKO_BASE_URL"] if env["AIKO_BASE_URL"]
24
+ merged[:model] = env["AIKO_MODEL"] if env["AIKO_MODEL"]
25
+ merged[:api_key] = env["AIKO_API_KEY"]
26
+ %i[api_key base_url model max_iterations workdir].each do |key|
27
+ merged[key] = cli_options[key] if cli_options.key?(key) && !cli_options[key].nil?
28
+ end
29
+ new(**merged)
30
+ end
31
+
32
+ def self.default_config_path
33
+ File.join(Dir.home, ".aiko", "config.json")
34
+ end
35
+
36
+ def self.load_file(path)
37
+ return {} unless File.exist?(path)
38
+
39
+ JSON.parse(File.read(path))
40
+ rescue JSON::ParserError => e
41
+ raise ConfigError, "invalid JSON in config file #{path}: #{e.message}"
42
+ end
43
+ private_class_method :load_file
44
+
45
+ def initialize(api_key:, base_url:, model:, max_iterations:, workdir: nil)
46
+ @api_key = api_key
47
+ @base_url = base_url
48
+ @model = model
49
+ @max_iterations = max_iterations
50
+ @workdir = File.expand_path(workdir || Dir.pwd)
51
+ validate!
52
+ end
53
+
54
+ private
55
+
56
+ def validate!
57
+ raise ConfigError, "API key is not set (set AIKO_API_KEY)" if @api_key.nil? || @api_key.empty?
58
+ raise ConfigError, "base_url must start with http:// or https://: #{@base_url}" unless %r{\Ahttps?://}.match?(@base_url)
59
+ unless @max_iterations.is_a?(Integer) && @max_iterations.positive?
60
+ raise ConfigError, "max_iterations must be a positive integer: #{@max_iterations.inspect}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ class Conversation
5
+ attr_reader :messages
6
+
7
+ def initialize(system_prompt:)
8
+ @system_prompt = system_prompt
9
+ @messages = [{ "role" => "system", "content" => system_prompt }]
10
+ end
11
+
12
+ def add_user(text)
13
+ @messages << { "role" => "user", "content" => text }
14
+ end
15
+
16
+ def add_assistant(message)
17
+ @messages << message
18
+ end
19
+
20
+ def add_tool_result(tool_call_id:, content:)
21
+ @messages << { "role" => "tool", "tool_call_id" => tool_call_id, "content" => content }
22
+ end
23
+
24
+ def clear
25
+ @messages = [{ "role" => "system", "content" => @system_prompt }]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ class Error < StandardError; end
5
+ class ConfigError < Error; end
6
+
7
+ module LLM
8
+ class APIError < Error; end
9
+ end
10
+
11
+ module Tools
12
+ class ToolError < Error; end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ module LLM
5
+ class Client
6
+ # messages: OpenAI互換形式のメッセージ配列
7
+ # tools: OpenAI互換形式のツール定義配列(空配列可)
8
+ # 戻り値: Aiko::LLM::Response
9
+ def chat(messages:, tools: [])
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Aiko
8
+ module LLM
9
+ class OpenAICompatible < Client
10
+ OPEN_TIMEOUT = 10
11
+ READ_TIMEOUT = 300
12
+ RETRY_WAITS = [1, 4].freeze
13
+
14
+ def initialize(api_key:, base_url:, model:, sleeper: ->(sec) { sleep(sec) })
15
+ super()
16
+ @api_key = api_key
17
+ @endpoint = URI.parse("#{base_url.sub(%r{/+\z}, "")}/chat/completions")
18
+ @model = model
19
+ @sleeper = sleeper
20
+ end
21
+
22
+ def chat(messages:, tools: [])
23
+ body = { "model" => @model, "messages" => messages }
24
+ body["tools"] = tools unless tools.empty?
25
+
26
+ status, response_body = post_with_retry(body)
27
+ unless (200..299).cover?(status)
28
+ raise APIError, "API request failed with status #{status}: #{response_body.to_s[0, 500]}"
29
+ end
30
+
31
+ to_response(parse_json(response_body))
32
+ end
33
+
34
+ private
35
+
36
+ def post_with_retry(body)
37
+ status, response_body = post_json(body)
38
+ RETRY_WAITS.each do |wait|
39
+ break unless status == 429 || (500..599).cover?(status)
40
+
41
+ @sleeper.call(wait)
42
+ status, response_body = post_json(body)
43
+ end
44
+ [status, response_body]
45
+ end
46
+
47
+ # テストではこのメソッドをオーバーライドしてHTTPをスタブする
48
+ def post_json(body)
49
+ http = Net::HTTP.new(@endpoint.host, @endpoint.port)
50
+ http.use_ssl = @endpoint.scheme == "https"
51
+ http.open_timeout = OPEN_TIMEOUT
52
+ http.read_timeout = READ_TIMEOUT
53
+
54
+ request = Net::HTTP::Post.new(@endpoint.request_uri)
55
+ request["Authorization"] = "Bearer #{@api_key}"
56
+ request["Content-Type"] = "application/json"
57
+ request.body = JSON.generate(body)
58
+
59
+ response = http.request(request)
60
+ [response.code.to_i, response.body]
61
+ rescue Net::OpenTimeout, Net::ReadTimeout, SystemCallError, OpenSSL::SSL::SSLError => e
62
+ raise APIError, "API request failed: #{e.class}: #{e.message}"
63
+ end
64
+
65
+ def parse_json(body)
66
+ JSON.parse(body)
67
+ rescue JSON::ParserError => e
68
+ raise APIError, "invalid JSON response: #{e.message}"
69
+ end
70
+
71
+ def to_response(json)
72
+ message = json.dig("choices", 0, "message")
73
+ raise APIError, "response has no choices: #{json.inspect[0, 500]}" unless message
74
+
75
+ Response.new(
76
+ content: message["content"],
77
+ tool_calls: (message["tool_calls"] || []).map { |tc| to_tool_call(tc) },
78
+ raw_message: message,
79
+ usage: json["usage"] || {}
80
+ )
81
+ end
82
+
83
+ def to_tool_call(tool_call)
84
+ function = tool_call.fetch("function", {})
85
+ arguments = JSON.parse(function["arguments"].to_s)
86
+ arguments = {} unless arguments.is_a?(Hash)
87
+ ToolCall.new(id: tool_call["id"], name: function["name"], arguments: arguments)
88
+ rescue JSON::ParserError => e
89
+ ToolCall.new(id: tool_call["id"], name: function["name"], arguments: {},
90
+ parse_error: e.message)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ module LLM
5
+ class ToolCall
6
+ attr_reader :id, :name, :arguments, :parse_error
7
+
8
+ def initialize(id:, name:, arguments:, parse_error: nil)
9
+ @id = id
10
+ @name = name
11
+ @arguments = arguments
12
+ @parse_error = parse_error
13
+ freeze
14
+ end
15
+ end
16
+
17
+ class Response
18
+ attr_reader :content, :tool_calls, :raw_message, :usage
19
+
20
+ def initialize(content:, tool_calls: [], raw_message: {}, usage: {})
21
+ @content = content.to_s
22
+ @tool_calls = tool_calls
23
+ @raw_message = raw_message
24
+ @usage = usage
25
+ freeze
26
+ end
27
+
28
+ def tool_calls?
29
+ !tool_calls.empty?
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aiko
4
+ module SystemPrompt
5
+ def self.build(workdir:)
6
+ <<~PROMPT
7
+ You are aiko, a coding agent that works in the user's project directory.
8
+ You accomplish tasks by calling the provided tools (reading/writing/editing
9
+ files, listing files, searching, and running shell commands).
10
+
11
+ Working directory: #{workdir}
12
+ All relative paths are resolved from this directory. You cannot access
13
+ files outside of it.
14
+
15
+ Guidelines for tool use:
16
+ - Read a file before you edit or overwrite it.
17
+ - Prefer edit_file for small changes and write_file for new files or rewrites.
18
+ - You may run commands to inspect the environment and to verify your work
19
+ (e.g. run the tests or execute the program you just wrote).
20
+ - If a tool returns an error, read it carefully and correct your approach.
21
+
22
+ Answer style:
23
+ - Be concise. After completing a task, summarize what you did and the result.
24
+ - Reply in the same language the user writes in.
25
+ PROMPT
26
+ end
27
+ end
28
+ end