Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions crates/ctl/src/commands/chat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Chat subcommands.

use {
crate::client::CtlClient,
serde_json::{Value, json},
};

pub async fn send(client: &mut CtlClient, text: &str, session_key: &str) -> anyhow::Result<Value> {
client
.call(
"chat.send",
json!({
"text": text,
"_session_key": session_key,
}),
)
.await
Comment on lines +8 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Agent loop exceeds RPC deadline

When a benchmark's model and tool loop takes longer than 120 seconds, chat.send remains subject to CtlClient::call's fixed deadline, causing moltis-ctl to exit with a timeout before the documented wait-for-completion behavior finishes.

Knowledge Base Used: CLI and Ctl: moltis binary and moltis-ctl gateway control

.map_err(Into::into)
}
Comment on lines +8 to +19

pub async fn history(client: &mut CtlClient, session_key: &str) -> anyhow::Result<Value> {
client
.call(
"chat.history",
json!({
"_session_key": session_key,
}),
)
.await
.map_err(Into::into)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn send_params_preserve_terminal_bench_session() {
let params = json!({
"text": "fix the task",
"_session_key": "terminal-bench:task-1",
});

assert_eq!(params["text"], "fix the task");
assert_eq!(params["_session_key"], "terminal-bench:task-1");
}
}
1 change: 1 addition & 0 deletions crates/ctl/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! CLI subcommand modules.

pub mod chat;
pub mod config;
pub mod health;
pub mod mcp;
Expand Down
23 changes: 23 additions & 0 deletions crates/ctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ struct Cli {

#[derive(Subcommand)]
enum Command {
/// Send a message through the Moltis agent loop and wait for completion.
Chat {
/// Message to send to the agent.
text: String,

/// Session key used to isolate this conversation.
#[arg(long, default_value = "terminal-bench")]
session_key: String,
},

/// Fetch persisted messages for a chat session.
ChatHistory {
/// Session key whose history should be returned.
#[arg(long, default_value = "terminal-bench")]
session_key: String,
},

/// Check gateway health.
Health,
/// Show gateway status.
Expand Down Expand Up @@ -90,6 +107,12 @@ async fn main() {
};

let result = match cli.command {
Command::Chat { text, session_key } => {
commands::chat::send(&mut client, &text, &session_key).await
},
Command::ChatHistory { session_key } => {
commands::chat::history(&mut client, &session_key).await
},
Command::Health => commands::health::health(&mut client).await,
Command::Status => commands::health::status(&mut client).await,
Command::Mcp(cmd) => commands::mcp::run(&mut client, cmd).await,
Expand Down
51 changes: 51 additions & 0 deletions tools/terminal_bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Running Moltis in Terminal-Bench / Harbor

`moltis-ctl chat` drives a task through the same authenticated WebSocket RPC and
agent loop as the web UI. The wrapper in this directory gives Harbor a simple
installed-agent entry point.

## Build

```sh
cargo build --release -p moltis-ctl
```

Make `target/release/moltis-ctl` available in the task container (or set
`MOLTIS_CTL_BIN` to its mounted path), then configure the installed-agent
command as:

```sh
tools/terminal_bench/run_moltis_agent.sh
```

The wrapper accepts the task instruction as arguments, from stdin, or via
`HARBOR_TASK_INSTRUCTION`.

## Required environment

- `MOLTIS_API_KEY`: gateway API key (required; do not commit it)
- `MOLTIS_GATEWAY_URL`: gateway URL, default
`http://host.docker.internal:13131`
- `MOLTIS_CTL_BIN`: `moltis-ctl` path, default `moltis-ctl`
- `MOLTIS_SESSION_KEY`: optional stable per-task session key; defaults to
`terminal-bench:${HARBOR_TASK_ID:-task}`

Each task must use a distinct session key so model/tool history cannot leak
between benchmark tasks.

## Smoke test

A representative end-to-end task can be run without Harbor orchestration:

```sh
MOLTIS_API_KEY='...' \
MOLTIS_GATEWAY_URL='http://127.0.0.1:13131' \
tools/terminal_bench/run_moltis_agent.sh \
'Create /tmp/terminal-bench-smoke.txt containing exactly moltis-ok'

test "$(cat /tmp/terminal-bench-smoke.txt)" = moltis-ok
```

The `chat` command does not return until the Moltis agent loop finishes. Its
JSON output includes the assistant text and token/round usage. Use
`moltis-ctl chat-history --session-key <key>` when debugging a failed run.
28 changes: 28 additions & 0 deletions tools/terminal_bench/run_moltis_agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

: "${MOLTIS_API_KEY:?MOLTIS_API_KEY must be set}"

MOLTIS_CTL_BIN="${MOLTIS_CTL_BIN:-moltis-ctl}"
MOLTIS_GATEWAY_URL="${MOLTIS_GATEWAY_URL:-http://host.docker.internal:13131}"
MOLTIS_SESSION_KEY="${MOLTIS_SESSION_KEY:-terminal-bench:${HARBOR_TASK_ID:-task}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shared fallback session state

When two benchmark invocations omit both MOLTIS_SESSION_KEY and HARBOR_TASK_ID, this fallback gives both terminal-bench:task; the gateway then reads and appends the same persisted conversation, leaking prior instructions and results into the later task's model context.

Knowledge Base Used: CLI and Ctl: moltis binary and moltis-ctl gateway control


Comment on lines +6 to +9
if (($#)); then
instruction="$*"
elif [[ -n "${HARBOR_TASK_INSTRUCTION:-}" ]]; then
instruction="$HARBOR_TASK_INSTRUCTION"
else
instruction="$(cat)"
fi

if [[ -z "${instruction//[[:space:]]/}" ]]; then
echo "terminal-bench: missing task instruction" >&2
exit 2
fi

exec "$MOLTIS_CTL_BIN" \
--gateway-url "$MOLTIS_GATEWAY_URL" \
--api-key "$MOLTIS_API_KEY" \
chat \
Comment on lines +23 to +26
--session-key "$MOLTIS_SESSION_KEY" \
"$instruction"
Comment on lines +23 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hyphenated instructions become options

A non-empty task instruction beginning with a hyphen is passed without an option terminator, so clap interprets it as an option rather than the text positional and exits before sending chat.send.

Suggested change
exec "$MOLTIS_CTL_BIN" \
--gateway-url "$MOLTIS_GATEWAY_URL" \
--api-key "$MOLTIS_API_KEY" \
chat \
--session-key "$MOLTIS_SESSION_KEY" \
"$instruction"
exec "$MOLTIS_CTL_BIN" \
--gateway-url "$MOLTIS_GATEWAY_URL" \
--api-key "$MOLTIS_API_KEY" \
chat \
--session-key "$MOLTIS_SESSION_KEY" \
-- \
"$instruction"

Knowledge Base Used: CLI and Ctl: moltis binary and moltis-ctl gateway control

44 changes: 44 additions & 0 deletions tools/terminal_bench/test_run_moltis_agent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

cat >"$tmp/moltis-ctl" <<'MOCK'
#!/usr/bin/env bash
printf '%s\n' "$@" >"${CAPTURE_FILE:?}"
MOCK
chmod +x "$tmp/moltis-ctl"

export CAPTURE_FILE="$tmp/args"
export MOLTIS_API_KEY="test-key"
export MOLTIS_CTL_BIN="$tmp/moltis-ctl"
export MOLTIS_GATEWAY_URL="http://gateway:13131"
export HARBOR_TASK_ID="representative-task"

"$script_dir/run_moltis_agent.sh" "write" "the file"

mapfile -t args <"$CAPTURE_FILE"
expected=(
--gateway-url http://gateway:13131
--api-key test-key
chat
--session-key terminal-bench:representative-task
"write the file"
)
[[ "${args[*]}" == "${expected[*]}" ]]

unset HARBOR_TASK_ID
export MOLTIS_SESSION_KEY="terminal-bench:stdin-task"
printf 'instruction from stdin\n' | "$script_dir/run_moltis_agent.sh"
mapfile -t args <"$CAPTURE_FILE"
[[ "${args[6]}" == "terminal-bench:stdin-task" ]]
[[ "${args[7]}" == "instruction from stdin" ]]

if MOLTIS_API_KEY='' "$script_dir/run_moltis_agent.sh" test 2>/dev/null; then
echo "expected missing API key to fail" >&2
exit 1
fi

echo "terminal-bench wrapper tests passed"