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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ ml-intern --max-iterations 100 "your prompt"
ml-intern --no-stream "your prompt"
```

**Attach local files and datasets:**

Use `--file` or `--image` when a local file should be visible to the next
agent turn only:

```bash
ml-intern "summarize this CSV" --file ./data.csv
ml-intern "what does this screenshot show?" --image ./screenshot.png
```

In interactive mode, queue local files for the next submitted message:

```text
/attach ./data.csv ./notes.txt
/attach ./screenshot.png
```

Use `--dataset` or `/dataset` when the file should be imported to a private
Hugging Face dataset repo for training jobs or later reuse:

```bash
ml-intern "fine-tune on this data" --dataset ./train.jsonl
```

```text
/dataset ./train.jsonl
```

Dataset imports are stored under a private repo named
`{username}/ml-intern-user-datasets`. Plain `--file`, `--image`, and `/attach`
do not upload to the Hub; they are per-turn context only. In the web UI, use
the paperclip button or drag and drop files into the composer, then choose
between **Attach to turn** and **Import as dataset** before sending.

Run `ml-intern` then `/model` to see the full list of suggested model ids
(Claude, GPT, HF-router models like MiniMax, Kimi, GLM, DeepSeek, and local
model prefixes).
Expand Down
41 changes: 36 additions & 5 deletions agent/core/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from agent.core.cost_estimation import CostEstimate, estimate_tool_cost
from agent.messaging.gateway import NotificationGateway
from agent.core import telemetry
from agent.core.attachments import build_user_content
from agent.core.doom_loop import check_for_doom_loop
from agent.core.hub_artifacts import start_session_artifact_collection_task
from agent.core.llm_params import _resolve_llm_params
Expand Down Expand Up @@ -1125,6 +1126,7 @@ async def _abandon_pending_approval(session: Session) -> None:
async def run_agent(
session: Session,
text: str,
attachments: list[dict[str, Any]] | None = None,
) -> str | None:
"""
Handle user input (like user_input_or_turn in codex.rs:1291)
Expand All @@ -1138,10 +1140,35 @@ async def run_agent(
if text and session.pending_approval:
await Handlers._abandon_pending_approval(session)

# Add user message to history only if there's actual content
if text:
user_msg = Message(role="user", content=text)
session.context_manager.add_message(user_msg)
redacted_user_msg: Message | None = None
raw_user_msg: Message | None = None

def _redact_transient_user_content() -> None:
if raw_user_msg is not None and redacted_user_msg is not None:
raw_user_msg.content = redacted_user_msg.content

# Add user message to history only if there's actual content. Image
# bytes are sent transiently for the current LLM turn, then replaced
# with text-only placeholders before the history is persisted/reused.
if text or attachments:
content = build_user_content(text, attachments or [])
redacted_text = build_user_content(text, [
m for m in (attachments or []) if m.get("type") == "dataset_import"
])
if attachments:
# Preserve manifest/preview context in the persisted text while
# avoiding raw image data URLs.
from agent.core.attachments import attachment_note

redacted_text = (text or "").rstrip() + attachment_note(attachments or [])
raw_user_msg = Message(role="user", content=content)
redacted_user_msg = Message(role="user", content=redacted_text)
if content != redacted_text:
session.context_manager.items.append(raw_user_msg)
if session.context_manager.on_message_added:
session.context_manager.on_message_added(redacted_user_msg)
else:
session.context_manager.add_message(raw_user_msg)

# Send event that we're processing
await session.send_event(
Expand Down Expand Up @@ -1596,6 +1623,7 @@ async def _exec_tool(
}

# Return early - wait for EXEC_APPROVAL operation
_redact_transient_user_content()
return None

iteration += 1
Expand Down Expand Up @@ -1637,6 +1665,8 @@ async def _exec_tool(
errored = True
break

_redact_transient_user_content()

if session.is_cancelled:
await _cleanup_on_cancel(session)
await session.send_event(Event(event_type="interrupted"))
Expand Down Expand Up @@ -1942,7 +1972,8 @@ async def process_submission(session: Session, submission) -> bool:

if op.op_type == OpType.USER_INPUT:
text = op.data.get("text", "") if op.data else ""
await Handlers.run_agent(session, text)
attachments = op.data.get("attachments", []) if op.data else []
await Handlers.run_agent(session, text, attachments)
return True

if op.op_type == OpType.COMPACT:
Expand Down
Loading
Loading