Skip to content

Add RPC for executing code#63

Merged
jmcphers merged 5 commits into
mainfrom
feature/execute-code-rpc
Apr 22, 2026
Merged

Add RPC for executing code#63
jmcphers merged 5 commits into
mainfrom
feature/execute-code-rpc

Conversation

@jmcphers
Copy link
Copy Markdown
Contributor

@jmcphers jmcphers commented Apr 8, 2026

This change adds a code execution API to Kallichore. Prior to this change, you needed to open a websocket to execute code; now you can do so using nothing but the REST API.

The new RPC accepts most of the same parameters as Jupyter's execute_request and returns the execute_result as data (along with all the other messages/data emitted during execution, such as stdout/stderr/display data/etc).

This is intended to help support code executions from clients like MCPs and Quarto; it will not be used in ordinary Positron consoles/notebooks.

Fixes #23.

@jmcphers jmcphers requested a review from samclark2015 April 8, 2026 23:08
Copy link
Copy Markdown

@samclark2015 samclark2015 left a comment

Choose a reason for hiding this comment

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

Overall, LGTM. One issue to fix with int overflow below.

Manual Tests

Tested against a live kcserver (no-auth mode) with an IPython 8.37 / Python 3.10.12 kernel.

# Scenario Command Result
1 Basic stdout run --code "print('hello world')" hello world captured in stream output
2 Expression result run --code "2 + 3" 5 in data["text/plain"]
3 stderr capture run --code "import sys; sys.stderr.write('error output\n')" ✅ stderr routed to stream with name: stderr
4 Error handling run --code "raise ValueError('test error')" status: Error, error_name/traceback populated
5 Sequential state run --code "x = 42" then run --code "print(x * 2)" ✅ state persisted across RPC calls, printed 84
6 display_data output run --code "from IPython.display import display; display({'text/plain': 'hello', 'text/html': '<b>hello</b>'}, raw=True)" ✅ both MIME types captured and returned
7 Timeout run --code "import time; time.sleep(30)" --timeout 2 ExecutionTimedOut after 2s, kernel interrupted
8 Post-timeout recovery run --code "print('still alive')" (after timeout) ✅ kernel still alive, execution succeeded
9 Session not found run --session-id nonexistent-session --code "..." SessionNotFound returned
10 Concurrent RPC + WebSocket listen in background, then run ✅ iopub messages forwarded to both; RPC and WebSocket independent

Comment on lines +231 to +239
/* Disabled because there's no example.
Some("ExecuteCode") => {
let result = rt.block_on(client.execute_code(
"session_id_example".to_string(),
???
));
info!("{:?} (X-Span-ID: {:?})", result, (client.context() as &dyn Has<XSpanIdString>).get().clone());
},
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we note this through a print statement? Right now, it looks like you'll get the "Invalid operation provided" despite being listed in the operation enum.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file is autogenerated by the OpenAPI generator and not used in the system (we could just gitignore it but could be helpful for docs/agents?)

// Collect output messages until we receive the execute_reply
let timeout_duration = execute_request
.timeout_seconds
.map(|s| std::time::Duration::from_secs(s as u64));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If timeout_seconds is negative, this will overflow to a hugely long duration. Is this expected/wanted?

Possible option, treat negatives like no timeout (returns None):

Suggested change
.map(|s| std::time::Duration::from_secs(s as u64));
.filter(|&s| s > 0)
.map(|s| std::time::Duration::from_secs(s as u64));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't know why someone would set a negative timeout? But we should have more reasonable behavior if they do! Fixed up.

@jmcphers jmcphers requested a review from samclark2015 April 10, 2026 20:42
@jmcphers jmcphers merged commit a19b978 into main Apr 22, 2026
6 of 13 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a non-Websocket API to execute code and return results

2 participants