Skip to content
Draft
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
8 changes: 8 additions & 0 deletions .agents/skills/develop-maple-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ build and performance evidence. Root `just agent-check`, `agent-build`, and
--no-update-lock-file` additionally validates workflow selection and security
contracts when CI, Nix, or routing changes.

`just test` and `just ci` prepare the pinned bundled CPython fixture and run its
worker and packaging suites. `just code-mode-smoke` exercises the actual worker;
direct Cargo worker tests require `just python-prepare` first. These commands
run from `apps/maple-agent/`. Runtime execution never downloads Python or falls
back to the system interpreter. Linux Nix packages retain their separately
declared CPython runtime closure; portable debug/archive layouts use the pinned
Python standalone distribution.

Agent has its own Cargo and Nix lockfiles. Shared Rust SDK/proxy runtime
changes must select Agent as well as the Research consumer; component-only
changes must not unnecessarily select Research packaging. Maintain the root
Expand Down
101 changes: 95 additions & 6 deletions .github/workflows/agent-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest-8-cores, macos-26, windows-latest]
include:
- os: ubuntu-latest-8-cores
focused: false
- os: macos-26
focused: false
- os: windows-latest
focused: false
- os: macos-15-intel
focused: true
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
Expand All @@ -102,6 +110,12 @@ jobs:
# Matches the component's frozen rust-overlay input.
toolchain: 1.98.0

- name: Install Windows build Python
if: runner.os == 'Windows'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.13'

- name: Cache Agent Rust dependencies
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
Expand All @@ -122,20 +136,39 @@ jobs:
fi
python3 ../../scripts/ci/verify-agent-rust-deps.py "$metadata"

# Linux runs these same worker and packaging suites through just ci.
- name: Prepare pinned Python and test worker packaging
if: runner.os != 'Linux'
run: |
if [ "$RUNNER_OS" = Windows ]; then
python scripts/prepare-python.py
python scripts/test-python-worker.py
python -m unittest discover -s scripts/tests
else
nix develop --no-update-lock-file . -c just python-test
nix develop --no-update-lock-file . -c python3 -m unittest discover -s scripts/tests
fi

- name: Format, lint, build, and test Linux feature matrix
if: runner.os == 'Linux'
run: nix develop --no-update-lock-file . -c just ci

- name: Build macOS workspace and test targets
if: runner.os == 'macOS'
if: runner.os == 'macOS' && !matrix.focused
run: nix develop --no-update-lock-file . -c cargo build --workspace --all-targets --locked

- name: Build and test focused Intel macOS worker
if: matrix.focused
run: |
nix develop --no-update-lock-file . -c cargo build -p maple-code-mode --all-targets --locked
nix develop --no-update-lock-file . -c cargo test -p maple-code-mode --locked

- name: Package and smoke macOS debug app
if: runner.os == 'macOS'
if: runner.os == 'macOS' && !matrix.focused
run: nix develop --no-update-lock-file . -c ./scripts/macos-debug-app.sh

- name: Test macOS workspace
if: runner.os == 'macOS'
if: runner.os == 'macOS' && !matrix.focused
run: nix develop --no-update-lock-file . -c cargo test --workspace --locked

- name: Build and test Windows workspace
Expand All @@ -144,16 +177,72 @@ jobs:
cargo build --workspace --all-targets --locked
cargo test --workspace --locked

- name: CodeMode debug smoke and relocated package without Python on PATH
run: |
if [ "$RUNNER_OS" = Windows ]; then
cargo run -p maple-code-mode --bin code-mode-smoke --locked
python scripts/check-python-package.py --smoke target/debug/code-mode-smoke.exe
else
nix develop --no-update-lock-file . -c just code-mode-smoke
nix develop --no-update-lock-file . -c python3 scripts/check-python-package.py --smoke target/debug/code-mode-smoke
fi

- name: Build Linux release binary
if: runner.os == 'Linux'
run: nix develop --no-update-lock-file . -c just release

- name: Stage complete Linux CI archive
if: runner.os == 'Linux'
run: |
nix develop --no-update-lock-file . -c python3 scripts/prepare-python.py \
--distribution pbs --destination target/release/runtime/python
nix develop --no-update-lock-file . -c python3 scripts/package-archive.py \
--binary target/release/maple-gpui --runtime target/release/runtime/python \
--name "maple-agent-linux-x86_64-ci-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
--output-dir target/ci-dist

# This is an unsigned CI build, never a GitHub Release or an updater feed.
- name: Upload Linux CI binary
- name: Upload complete Linux CI archive
if: runner.os == 'Linux'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: maple-agent-linux-x86_64-ci-${{ github.run_id }}-${{ github.run_attempt }}
path: apps/maple-agent/target/release/maple-gpui
path: apps/maple-agent/target/ci-dist/*
if-no-files-found: error
retention-days: 5

nix-python:
name: Agent Nix runtime and package (Linux ARM64)
needs: changes
if: ${{ always() && !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false') }}
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Install pinned Nix environment
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
with:
github-token: ""

- name: Build package with its retained interpreter
run: nix build --no-update-lock-file .#default

- name: Verify installed closure and native worker
run: |
manifest="$(readlink -f result)/share/maple-gpui/python/runtime.json"
export MAPLE_CODE_MODE_DISTRIBUTION=nix
export MAPLE_CODE_MODE_RUNTIME_MANIFEST="$manifest"
nix develop --no-update-lock-file . -c just python-test
nix develop --no-update-lock-file . -c cargo test -p maple-code-mode --locked
nix develop --no-update-lock-file . -c just code-mode-smoke
nix path-info --recursive ./result > closure.txt
nix develop --no-update-lock-file . -c python3 - "$manifest" <<'PYTHON'
import json, pathlib, subprocess, sys
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text())
interpreter = pathlib.Path(manifest['executable'])
assert str(interpreter.parents[1]) in pathlib.Path('closure.txt').read_text().splitlines()
subprocess.run([str(interpreter), '-I', '-B', '-c', 'import ssl, sqlite3, ctypes, zlib, bz2, lzma'], check=True, env={})
PYTHON
2 changes: 2 additions & 0 deletions apps/maple-agent/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Keep pinned upstream license notices byte-for-byte, including blank EOF lines.
scripts/python-licenses/* whitespace=-blank-at-eof
32 changes: 24 additions & 8 deletions apps/maple-agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/maple-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["crates/maple-agent", "crates/maple-billing", "app"]
members = ["crates/maple-agent", "crates/maple-billing", "crates/maple-code-mode", "app"]

[workspace.package]
edition = "2024"
Expand Down
25 changes: 13 additions & 12 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ crates/maple-agent/ Maple's transport-neutral agent runtime, extracted from
tools, permission policy, account-scoped session
storage, and the ACP server.
crates/maple-billing/ HTTP client for the Maple billing API.
docs/ Theme spec measured from the Tauri app.
scripts/ One maintainer helper: screenshot.py takes a desktop
screenshot through the xdg portal on GNOME Wayland.
Nothing in the build or the app uses it.
crates/maple-code-mode/ Bundled CPython worker, protocol, and process lifecycle.
docs/ Product and implementation notes, including the theme.
scripts/ Python preparation, packaging and validation helpers;
macOS debug-app staging; optional Wayland screenshots.
```

### Backend / frontend boundary
Expand All @@ -43,7 +43,8 @@ remove Tauri:
sink are injectable traits
- public visibility opened on the service surface the app consumes

Goose is pinned to the aaif-goose fork revision recorded in this component’s
Goose is pinned to an aaif-goose fork with Maple’s native-client integration and
opt-in ordered tool scheduling. Its exact revision is recorded in this component’s
Cargo manifests and lockfile; Research has an independent dependency graph.

## Features
Expand All @@ -56,18 +57,18 @@ Cargo manifests and lockfile; Research has an independent dependency graph.
- Agent chat with streaming Markdown, tool calls, permission prompts,
agent questions, image attachments (picker, paste, or drag and drop),
a per-message Copy button, and a context-window indicator.
- [Python scratchpad](docs/python-code-mode.md): the normal `python_code`
tool uses bundled CPython with persistent task state and top-level await.
Existing permissions apply; the task menu can reset retained Python state.
- Slash commands in the composer: `/btw` asks a side question the task
never sees, plus `/compact`, `/new`, `/pin`, `/web`, `/model`, and
`/help`. The account's skills appear in the same list.
- The task's latest todo list stays pinned above the composer.
- Subagents: the task can give a piece of work to a subagent with the
`delegate` tool, which runs it in its own context. Known limitation:
a subagent does not inherit the task's permission mode. Goose runs
every subagent with all tools approved, so even in Read only mode a
subagent can run shell commands and edit files without a prompt. The
fix needs the Goose fork to forward subagent approvals to the parent
(summon.rs hard-codes Auto because an approval would hang). The
subagents that work now show above the composer with the tool each one
`delegate` tool, which runs it in its own context. The pinned Goose fork
inherits the parent's permission mode and forwards child approvals. Its
independently constructed clients do not receive Maple's Python capability.
Subagents show above the composer with the tool each one
runs and how long it has worked. A subagent that runs in the background
keeps its row after the turn ends, and Maple tells the task when it
finishes: into the running turn, or into the next one.
Expand Down
36 changes: 29 additions & 7 deletions apps/maple-agent/app/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ use std::sync::Arc;
use maple_agent::agent::{
AgentCreateSessionRequest, AgentDesktopQueueSnapshot, AgentEventSink, AgentIntegration,
AgentIntegrationPermissionKind, AgentIntegrationPermissions, AgentProjectRootRegistration,
AgentProjectTrustStatus, AgentQueueControlRequest, AgentRenameSessionRequest,
AgentRuntimeStatus, AgentSendMessageRequest, AgentServiceEvent, AgentSessionDetail,
AgentSessionSummary, AgentSetIntegrationEnabledRequest, AgentSetupIntegrationRequest,
AgentSlashCommand, AgentStartRequest, AgentSubagent, MapleAgentHostResources,
MapleAgentService, RecentProjectRoot,
AgentProjectTrustStatus, AgentPythonStatus, AgentQueueControlRequest,
AgentRenameSessionRequest, AgentRuntimeStatus, AgentSendMessageRequest, AgentServiceEvent,
AgentSessionDetail, AgentSessionSummary, AgentSetIntegrationEnabledRequest,
AgentSetupIntegrationRequest, AgentSlashCommand, AgentStartRequest, AgentSubagent,
MapleAgentHostResources, MapleAgentService, RecentProjectRoot,
};
use maple_agent::maple_api::{
MapleApiAuthEventSink, MapleApiAuthRequest, MapleApiAuthSnapshot, MapleApiAuthState,
Expand All @@ -50,8 +50,8 @@ pub struct PendingPermission {
pub request_id: String,
pub tool_name: String,
pub prompt: Option<String>,
/// Pretty-printed tool arguments, formatted once when the request
/// arrives instead of on every frame.
/// Prepared tool arguments, including literal multiline Python source,
/// formatted once when the request arrives instead of on every frame.
pub arguments: Arc<str>,
}

Expand Down Expand Up @@ -1716,6 +1716,28 @@ impl AgentBackend {
.await
}

/// Read the current task's reset availability without starting Python.
pub async fn python_status(
&self,
user_id: &str,
session_id: &str,
) -> Result<AgentPythonStatus, String> {
self.service
.handle_for_user(user_id)
.await?
.python_status(session_id.to_string())
.await
}

/// Reset the currently retained task state; success means cleanup completed.
pub async fn reset_python(&self, user_id: &str, session_id: &str) -> Result<(), String> {
self.service
.handle_for_user(user_id)
.await?
.reset_python(session_id.to_string())
.await
}

/// The subagents still working for a task. A task whose run ended can
/// still have a background subagent; this rebuilds the card for it.
pub async fn session_subagents(
Expand Down
Loading
Loading