Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `pctx mcp start --stateful-http`: HTTP mode now supports stateful upstream sessions scoped to the MCP session ID. The connection pool is created on the first request and reused for the lifetime of the HTTP session, then torn down when the client sends a `DELETE`.
- `pctx mcp start --stdio`: When running as a stdio MCP server (e.g. in Claude Desktop), a single global session is used for the entire process lifetime — upstream MCP servers connect once and stay connected until `pctx` exits.
- Session server (`pctx start`): upstream connection pools are now scoped per code mode session — created on first use and cleaned up when the session is deleted.
- `ExecuteTypescriptOutput` now includes a `trace` field: a structured record of everything that happened during the execution — the type-check phase, each upstream MCP tool call (including whether the client was served from the connection pool cache), and each callback invocation — all with start/end timestamps.

### Changed

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ help:
@echo " make build-python - Build Python package (resolves symlinks before build)"
@echo ""

# Generate CLI and Python documentation
# Generate CLI, OAS, and Python documentation
docs:
@./scripts/generate-cli-docs.sh
@echo ""
@./scripts/generate-openapi.sh
@echo ""
@echo "Building Python Sphinx documentation..."
@cd pctx-py && uv run sphinx-build -b html docs docs/_build/html
@echo ""
Expand Down
32 changes: 13 additions & 19 deletions crates/pctx_code_mode/src/code_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use tracing::{debug, info, instrument, warn};
use crate::{
Error, Result,
model::{
CallbackConfig, ExecuteOutput, FunctionDetails, GetFunctionDetailsInput,
GetFunctionDetailsOutput, ListFunctionsOutput, ListedFunction,
CallbackConfig, ExecuteBashOutput, ExecuteTypescriptOutput, FunctionDetails,
GetFunctionDetailsInput, GetFunctionDetailsOutput, ListFunctionsOutput, ListedFunction,
},
};

Expand Down Expand Up @@ -480,7 +480,7 @@ impl CodeMode {

/// Execute bash commands directly in the virtual filesystem
#[instrument(skip(self), ret(Display), err)]
pub async fn execute_bash(&self, command: &str) -> Result<ExecuteOutput> {
pub async fn execute_bash(&self, command: &str) -> Result<ExecuteBashOutput> {
debug!(command = %command, "Executing bash command");

// Wrap bash command in async IIFE and export the result
Expand Down Expand Up @@ -509,7 +509,7 @@ export default result;"#,

// Extract stdout and stderr from the bash result object
// The output field contains the result object: { stdout, stderr, exitCode }
let (bash_stdout, bash_stderr, exit_code) = if execution_res.success {
let (stdout, stderr, exit_code) = if execution_res.success {
if let Some(output_value) = &execution_res.output {
if let Some(result_obj) = output_value.as_object() {
let stdout = result_obj
Expand Down Expand Up @@ -538,23 +538,16 @@ export default result;"#,
(String::new(), execution_res.stderr.clone(), 1)
};

let success = execution_res.success && exit_code == 0;

if success {
if exit_code == 0 {
debug!("Bash execution completed successfully");
} else {
warn!(
"Bash execution failed with exit code {}: {}",
exit_code, bash_stderr
);
warn!("Bash execution failed with exit code {exit_code}: {stderr}");
}

Ok(ExecuteOutput {
success,
stdout: bash_stdout,
stderr: bash_stderr,
output: None,
registry: Default::default(),
Ok(ExecuteBashOutput {
exit_code,
stdout,
stderr,
})
}

Expand All @@ -565,7 +558,7 @@ export default result;"#,
code: &str,
disclosure: ToolDisclosure,
registry: Option<PctxRegistry>,
) -> Result<ExecuteOutput> {
) -> Result<ExecuteTypescriptOutput> {
let registry: PctxRegistry = if let Some(r) = registry {
r
} else {
Expand Down Expand Up @@ -685,12 +678,13 @@ export default result;"#,
warn!("TypeScript execution failed: {:?}", execution_res.stderr);
}

Ok(ExecuteOutput {
Ok(ExecuteTypescriptOutput {
success: execution_res.success,
stdout: execution_res.stdout,
stderr: execution_res.stderr,
output: execution_res.output,
registry: execution_res.registry,
trace: execution_res.trace,
})
}
}
50 changes: 39 additions & 11 deletions crates/pctx_code_mode/src/model.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::fmt::Display;

pub use pctx_executor::events::*;
use pctx_registry::PctxRegistry;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;
use utoipa::ToSchema;

// -------------- List Functions --------------
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ListFunctionsOutput {
/// Available functions
pub functions: Vec<ListedFunction>,
Expand Down Expand Up @@ -89,13 +90,13 @@ impl<'de> Deserialize<'de> for FunctionId {
}
}

#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct GetFunctionDetailsOutput {
pub functions: Vec<FunctionDetails>,

pub code: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct FunctionDetails {
#[serde(flatten)]
pub listed: ListedFunction,
Expand All @@ -118,10 +119,13 @@ pub struct ExecuteBashInput {
pub command: String,
}

#[deprecated(note = "Use `ExecuteTypescriptInput` instead")]
pub type ExecuteInput = ExecuteTypescriptInput;

#[allow(clippy::doc_markdown)]
#[derive(Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(default)]
pub struct ExecuteInput {
pub struct ExecuteTypescriptInput {
/// Typescript code to execute.
///
/// REQUIRED FORMAT:
Expand All @@ -137,23 +141,47 @@ pub struct ExecuteInput {
}

#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ExecuteOutput {
pub struct ExecuteBashOutput {
/// Exit code of the bash command
pub exit_code: i64,
/// Standard output of executed bash command
pub stdout: String,
/// Standard error of executed bash command
pub stderr: String,
}

impl Display for ExecuteBashOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Exit Code: {}\n\n# STDOUT\n{}\n\n# STDERR\n{}",
&self.exit_code, &self.stdout, &self.stdout
)
}
}

#[deprecated(note = "Use `ExecuteTypescriptOutput` instead")]
pub type ExecuteOutput = ExecuteTypescriptOutput;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecuteTypescriptOutput {
/// Success of executed code
pub success: bool,
/// Standard output of executed code
pub stdout: String,
/// Standard error of executed code
pub stderr: String,
/// Value returned by executed function
#[schema(value_type = Object)]
pub output: Option<serde_json::Value>,

/// Trace of events during execution
pub trace: ExecutionTrace,

/// Registry used in execution
#[serde(skip)]
#[schemars(skip)]
#[schema(ignore)]
pub registry: PctxRegistry,
}
impl ExecuteOutput {
impl ExecuteTypescriptOutput {
pub fn markdown(&self) -> String {
format!(
"Code Executed Successfully: {success}
Expand All @@ -177,9 +205,9 @@ impl ExecuteOutput {
)
}
}
impl Display for ExecuteOutput {
impl Display for ExecuteTypescriptOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", json!(&self))
write!(f, "{}", self.markdown())
}
}

Expand Down
70 changes: 70 additions & 0 deletions crates/pctx_executor/src/events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::time::SystemTime;

pub use pctx_registry::events::*;
use serde::Deserialize;

use crate::{Diagnostic, Serialize};

/// The outcome of the type-check phase.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum TypecheckOutcome {
#[serde(rename = "passed")]
Passed,
#[serde(rename = "failed")]
Failed { diagnostics: Vec<Diagnostic> },
}

/// A timed record of the type-check phase.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeCheckEvent {
pub started_at: SystemTime,
pub ended_at: SystemTime,
pub outcome: TypecheckOutcome,
}

/// A single event in an execution flow.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ExecutionEvent {
#[serde(rename = "type_check")]
TypeCheck(TypeCheckEvent),
#[serde(rename = "mcp_tool_call")]
McpToolCall(McpToolCallEvent),
#[serde(rename = "callback_invocation")]
CallbackInvocation(CallbackInvocationEvent),
}

impl ExecutionEvent {
pub fn started_at(&self) -> SystemTime {
match self {
ExecutionEvent::TypeCheck(e) => e.started_at,
ExecutionEvent::McpToolCall(e) => e.started_at,
ExecutionEvent::CallbackInvocation(e) => e.started_at,
}
}
}

/// A complete trace of one TypeScript execution flow.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionTrace {
/// The original TypeScript script that was submitted.
pub code: String,
/// When `execute()` was called.
pub started_at: SystemTime,
/// When `execute()` returned.
pub ended_at: SystemTime,
/// All events that occurred during the flow, sorted by `started_at`.
pub events: Vec<ExecutionEvent>,
}

impl Default for ExecutionTrace {
fn default() -> Self {
Self {
code: String::new(),
started_at: SystemTime::UNIX_EPOCH,
ended_at: SystemTime::UNIX_EPOCH,
events: Vec::new(),
}
}
}
Loading
Loading