feat(ai): support opencode agent selection
This commit is contained in:
parent
835f6f012d
commit
f44aebf5d5
@ -2,8 +2,10 @@
|
||||
description: Answers in-game ComputerCraft and TrapOS user questions with concise, actionable replies; use for programs/ai.lua requests.
|
||||
mode: primary
|
||||
permission:
|
||||
edit: allow
|
||||
bash: allow
|
||||
"*": deny
|
||||
websearch: allow
|
||||
computercraft-mcp-bridge_probe-computers: allow
|
||||
computercraft-mcp-bridge_exec-lua: allow
|
||||
---
|
||||
|
||||
You answer ComputerCraft / CC:Tweaked users from inside Minecraft through TrapOS `ai`.
|
||||
@ -18,7 +20,17 @@ When giving commands, make them directly runnable in CraftOS when possible.
|
||||
|
||||
When giving code, keep it minimal and runnable. Avoid markdown fences unless the user clearly asks for a code block.
|
||||
|
||||
You may use repository tools, bash, and edits when asked to test or change TrapOS code. Keep tool-driven investigation focused and summarize only the result.
|
||||
You may use web search for current external facts and documentation. Keep searches focused and summarize only what unblocks the user.
|
||||
|
||||
You may use the ComputerCraft MCP bridge only through `probe-computers` and `exec-lua`.
|
||||
|
||||
Use `probe-computers` before `exec-lua` unless the target computer id is already clear from the conversation.
|
||||
|
||||
Treat `exec-lua` as privileged in-game execution. Prefer read-only inspection. Do not delete files, reboot or shut down computers, move turtles, change inventories, transmit network traffic, mutate peripherals, or run long loops unless the user explicitly asks for that specific effect.
|
||||
|
||||
Keep `exec-lua` snippets small and bounded. Use short timeouts. Avoid blocking pulls, sleeps, infinite loops, and assumptions that a timeout stops code already running in ComputerCraft.
|
||||
|
||||
`print()` and `write()` output is captured in MCP results. To intentionally write to the visible ComputerCraft screen, use terminal APIs such as `term.clear()`, `term.setCursorPos()`, and `term.write()`.
|
||||
|
||||
If the user asks for repo internals, answer only what is needed to unblock them.
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ local DEFAULT_POLL_INTERVAL_SECONDS = 2;
|
||||
local DEFAULT_LUA_EXEC_MAX_RETRIES = 2;
|
||||
local DEFAULT_LUA_EXEC_TIMEOUT_SECONDS = 5;
|
||||
local DEFAULT_SESSION_SETTING_KEY = 'opencc.session_id';
|
||||
local DEFAULT_AGENT_SETTING_KEY = 'opencc.agent';
|
||||
|
||||
local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
@ -211,6 +212,13 @@ local function createAi(opts)
|
||||
return providerId, modelId;
|
||||
end
|
||||
|
||||
local function resolveAgent(options)
|
||||
local agent = options.agent;
|
||||
if agent == nil then agent = settingsLib.get(DEFAULT_AGENT_SETTING_KEY); end
|
||||
if isBlank(agent) then return nil; end
|
||||
return agent;
|
||||
end
|
||||
|
||||
local function resolveConfig(options)
|
||||
local url = options.serverUrl or settingsLib.get('opencc.server_url');
|
||||
if not url or url == '' then
|
||||
@ -225,6 +233,7 @@ local function createAi(opts)
|
||||
password = password,
|
||||
providerID = providerId,
|
||||
modelID = modelId,
|
||||
agent = resolveAgent(options),
|
||||
timeoutSeconds = resolveTimeout(options),
|
||||
pollTimeoutSeconds = resolvePollTimeout(options),
|
||||
pollIntervalSeconds = resolvePollInterval(options),
|
||||
@ -239,6 +248,9 @@ local function createAi(opts)
|
||||
if cfg.providerID and cfg.modelID then
|
||||
body.model = { providerID = cfg.providerID, modelID = cfg.modelID };
|
||||
end
|
||||
if cfg.agent then
|
||||
body.agent = cfg.agent;
|
||||
end
|
||||
return body;
|
||||
end
|
||||
|
||||
@ -249,6 +261,9 @@ local function createAi(opts)
|
||||
if cfg.providerID and cfg.modelID then
|
||||
body.model = { providerID = cfg.providerID, modelID = cfg.modelID };
|
||||
end
|
||||
if cfg.agent then
|
||||
body.agent = cfg.agent;
|
||||
end
|
||||
return body;
|
||||
end
|
||||
|
||||
@ -609,6 +624,7 @@ local function createAi(opts)
|
||||
password = options.password,
|
||||
providerID = options.providerID,
|
||||
modelID = options.modelID,
|
||||
agent = options.agent,
|
||||
timeoutSeconds = options.timeoutSeconds,
|
||||
};
|
||||
end
|
||||
|
||||
@ -49,6 +49,12 @@ set opencc.provider_id anthropic
|
||||
set opencc.model_id claude-opus-4-7
|
||||
```
|
||||
|
||||
Optional agent setting for the in-game ComputerCraft assistant:
|
||||
|
||||
```sh
|
||||
set opencc.agent computercraft
|
||||
```
|
||||
|
||||
Test it:
|
||||
|
||||
```sh
|
||||
|
||||
@ -85,11 +85,12 @@ Send a message and wait for the AI reply (blocking). Returns when the assistant
|
||||
"parts": [
|
||||
{ "type": "text", "text": "your prompt here" }
|
||||
],
|
||||
"agent": "computercraft",
|
||||
"model": { "providerID": "anthropic", "modelID": "claude-opus-4-7" }
|
||||
}
|
||||
```
|
||||
|
||||
`model` is optional — omit to use the server's configured default.
|
||||
`agent` and `model` are optional — omit them to use the server's configured defaults.
|
||||
|
||||
**Response** `200`:
|
||||
```json
|
||||
@ -149,13 +150,14 @@ Fire-and-forget variant. Returns `204` immediately. Include `messageID` in the r
|
||||
"parts": [
|
||||
{ "type": "text", "text": "your prompt here" }
|
||||
],
|
||||
"agent": "computercraft",
|
||||
"model": { "providerID": "anthropic", "modelID": "claude-opus-4-7" }
|
||||
}
|
||||
```
|
||||
|
||||
Unlike `/message`, `model` is **not** optional in practice — omitting it causes the request to be accepted (`204`) without triggering generation, so the assistant message never appears. `ai` only uses this async endpoint when `opencc.provider_id` and `opencc.model_id` are configured; otherwise it falls back to blocking `POST /session/:id/message`.
|
||||
|
||||
When async mode is available, `ai` uses this endpoint to avoid `504` failures from the blocking `/message` endpoint when the LLM takes longer than one HTTP request timeout. The submitted `messageID` identifies the user or assistant message depending on the opencode response shape; `ai` polls `GET /session/:id/message` and reads the completed assistant message.
|
||||
When async mode is available, `ai` uses this endpoint to avoid `504` failures from the blocking `/message` endpoint when the LLM takes longer than one HTTP request timeout. The submitted `messageID` identifies the user or assistant message depending on the opencode response shape; `ai` polls `GET /session/:id/message` and reads the completed assistant message. If `opencc.agent` or `--agent <name>` is set, `ai` includes it as `agent` in the request body.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -104,6 +104,12 @@ Optional — override the Basic Auth username (default `opencode`):
|
||||
set opencc.username myuser
|
||||
```
|
||||
|
||||
Optional — select an opencode agent for requests from this computer:
|
||||
|
||||
```sh
|
||||
set opencc.agent computercraft
|
||||
```
|
||||
|
||||
Optional but recommended: pick the provider and model. When both are set, `ai` posts to `/session/:id/prompt_async` and polls for completion. Without them, `ai` falls back to blocking `/session/:id/message`, which can use the server default model but is more exposed to HTTP timeouts:
|
||||
|
||||
```sh
|
||||
@ -132,6 +138,7 @@ ai ping -- ping, reuses existing session
|
||||
ai "explain what a turtle is"
|
||||
ai new "start a fresh topic" -- forget current session, start fresh
|
||||
ai sessions -- list all server sessions with their IDs
|
||||
ai --agent computercraft "what peripherals are attached?"
|
||||
ai --verbose ping -- show HTTP/session diagnostics
|
||||
ai --help
|
||||
ai --version
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.7",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
"trapos-boot": "0.3.2",
|
||||
"trapos-net": "0.3.0",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.6.4",
|
||||
"trapos-ai": "0.6.5",
|
||||
"trapos-sandbox": "0.1.4",
|
||||
"trapos": "0.8.6"
|
||||
"trapos": "0.8.7"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-ai",
|
||||
"version": "0.6.4",
|
||||
"version": "0.6.5",
|
||||
"description": "TrapOS AI client for opencode serve",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.7",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
||||
"files": [],
|
||||
|
||||
@ -4,14 +4,25 @@ local createVersion = require('/apis/libversion');
|
||||
local rawArgs = table.pack(...);
|
||||
local args = { n = 0 };
|
||||
local verbose = false;
|
||||
local agentName = nil;
|
||||
local parseError = nil;
|
||||
|
||||
for i = 1, rawArgs.n do
|
||||
if rawArgs[i] == '--verbose' or rawArgs[i] == '-v' then
|
||||
local argIndex = 1;
|
||||
while argIndex <= rawArgs.n do
|
||||
if rawArgs[argIndex] == '--verbose' or rawArgs[argIndex] == '-v' then
|
||||
verbose = true;
|
||||
elseif rawArgs[argIndex] == '--agent' or rawArgs[argIndex] == '-a' then
|
||||
if argIndex == rawArgs.n then
|
||||
parseError = 'missing agent name after ' .. rawArgs[argIndex];
|
||||
else
|
||||
argIndex = argIndex + 1;
|
||||
agentName = rawArgs[argIndex];
|
||||
end
|
||||
else
|
||||
args.n = args.n + 1;
|
||||
args[args.n] = rawArgs[i];
|
||||
args[args.n] = rawArgs[argIndex];
|
||||
end
|
||||
argIndex = argIndex + 1;
|
||||
end
|
||||
|
||||
local function printUsage()
|
||||
@ -25,6 +36,7 @@ local function printUsage()
|
||||
print(' ai --lua-exec <prompt> (deprecated)');
|
||||
print(' ai sessions');
|
||||
print(' ai --sessions');
|
||||
print(' ai --agent <name> <command>');
|
||||
print(' ai --verbose <command>');
|
||||
print(' ai --version');
|
||||
print(' ai --help');
|
||||
@ -36,6 +48,7 @@ local function printUsage()
|
||||
print(' opencc.username (default: opencode)');
|
||||
print(' opencc.password (Basic Auth password)');
|
||||
print(' opencc.session_id (auto-managed)');
|
||||
print(' opencc.agent (e.g. computercraft)');
|
||||
print(' opencc.provider_id (e.g. anthropic)');
|
||||
print(' opencc.model_id (e.g. claude-opus-4-7)');
|
||||
print(' opencc.timeout_seconds (per HTTP call, max 60)');
|
||||
@ -48,10 +61,16 @@ local function printAiLog(message)
|
||||
end
|
||||
|
||||
local function askOptions()
|
||||
local options = nil;
|
||||
if verbose then
|
||||
return { log = printAiLog };
|
||||
options = options or {};
|
||||
options.log = printAiLog;
|
||||
end
|
||||
return nil;
|
||||
if agentName then
|
||||
options = options or {};
|
||||
options.agent = agentName;
|
||||
end
|
||||
return options;
|
||||
end
|
||||
|
||||
local function joinArgs(start)
|
||||
@ -135,6 +154,12 @@ if command == '--help' or command == '-help' or command == 'help' then
|
||||
return;
|
||||
end
|
||||
|
||||
if parseError then
|
||||
print(parseError);
|
||||
printUsage();
|
||||
return;
|
||||
end
|
||||
|
||||
if args.n == 0 then
|
||||
printUsage();
|
||||
return;
|
||||
|
||||
80
tests/ai.lua
80
tests/ai.lua
@ -394,6 +394,60 @@ testlib.test('ask sends exact prompt text', function()
|
||||
testlib.assertEquals(body.parts[1].text, 'my prompt');
|
||||
end);
|
||||
|
||||
testlib.test('ask includes agent from settings', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ messageResp('reply') },
|
||||
{}
|
||||
);
|
||||
local settingsStub = fakeSettings({
|
||||
['opencc.server_url'] = 'http://host',
|
||||
['opencc.session_id'] = 'ses_1',
|
||||
['opencc.agent'] = 'computercraft',
|
||||
});
|
||||
local ai = createAi({ http = httpStub, settings = settingsStub });
|
||||
|
||||
ai.ask('hello');
|
||||
|
||||
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
||||
testlib.assertEquals(body.agent, 'computercraft');
|
||||
end);
|
||||
|
||||
testlib.test('ask option agent overrides settings', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ messageResp('reply') },
|
||||
{}
|
||||
);
|
||||
local settingsStub = fakeSettings({
|
||||
['opencc.server_url'] = 'http://host',
|
||||
['opencc.session_id'] = 'ses_1',
|
||||
['opencc.agent'] = 'build',
|
||||
});
|
||||
local ai = createAi({ http = httpStub, settings = settingsStub });
|
||||
|
||||
ai.ask('hello', { agent = 'computercraft' });
|
||||
|
||||
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
||||
testlib.assertEquals(body.agent, 'computercraft');
|
||||
end);
|
||||
|
||||
testlib.test('ask omits blank agent setting', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ messageResp('reply') },
|
||||
{}
|
||||
);
|
||||
local settingsStub = fakeSettings({
|
||||
['opencc.server_url'] = 'http://host',
|
||||
['opencc.session_id'] = 'ses_1',
|
||||
['opencc.agent'] = ' ',
|
||||
});
|
||||
local ai = createAi({ http = httpStub, settings = settingsStub });
|
||||
|
||||
ai.ask('hello');
|
||||
|
||||
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
||||
testlib.assertEquals(body.agent, nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask includes model when provider_id and model_id are set', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ messageResp('reply') },
|
||||
@ -507,6 +561,32 @@ testlib.test('ask generates opencode-compatible message ids', function()
|
||||
testlib.assertTrue(string.find(body.messageID, '^msg_') ~= nil);
|
||||
end);
|
||||
|
||||
testlib.test('ask includes agent in async prompts', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ asyncResp() },
|
||||
{
|
||||
messageListResp({ assistantMessage('msg_1', 'reply', true) }),
|
||||
}
|
||||
);
|
||||
local settingsStub = fakeAsyncSettings({
|
||||
['opencc.session_id'] = 'ses_1',
|
||||
['opencc.agent'] = 'computercraft',
|
||||
});
|
||||
local elFactory = fakeEventloopFactory();
|
||||
local ai = createAi({
|
||||
http = httpStub,
|
||||
settings = settingsStub,
|
||||
now = function() return 10; end,
|
||||
eventloop = elFactory,
|
||||
});
|
||||
|
||||
local ok = ai.ask('hello', { messageId = 'msg_1' });
|
||||
|
||||
testlib.assertTrue(ok);
|
||||
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
||||
testlib.assertEquals(body.agent, 'computercraft');
|
||||
end);
|
||||
|
||||
testlib.test('ask polls async message until completion', function()
|
||||
local httpStub = fakeHttp(
|
||||
{ sessionResp('ses_1'), asyncResp() },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user