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
6 changes: 3 additions & 3 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ CodeSpace는 외부 코딩 에이전트가 작업 공간의 파일을 읽고 수
| --- | --- |
| 실행 환경과 파일 확인 | `workspace_info`, `find`, `read` |
| 패치 적용과 기록된 상태 조회 | `apply_patch`, `operation_status` |
| 프로세스 실행과 제어 | `exec_command`, `read_process`, `write_stdin`, `terminate_process` |
| 프로세스 실행과 제어 | `exec_command`, `read_process`, `process_status`, `write_stdin`, `terminate_process` |
| 작업과 사용자 추가 지시 관리 | `work_open`, `steer_status`, `steer_claim_next`, `steer_complete`, `work_finish` |

두 전송 방식에서 같은 도구를 사용할 수 있습니다. HTTP `/inbox` API는 사용자용 클라이언트가 지시 초안을 관리하는 JSON API입니다. 브라우저에서 사용하는 받은 편지함 화면은 제공하지 않습니다.
Expand All @@ -35,8 +35,8 @@ CodeSpace는 외부 코딩 에이전트가 작업 공간의 파일을 읽고 수

에이전트를 연동할 때는 다음 제약을 반영해야 합니다.

- 프로세스 결과에는 출력과 EOF가 있지만 종료 코드는 없습니다. EOF만으로 테스트 성공을 판단할 수 없습니다.
- 프로세스 출력의 보관 크기가 제한되어 있으며, 유실된 출력을 알리는 별도 필드는 없습니다.
- 프로세스 종료는 `process_status`로 판정하세요. `read_process`의 EOF는 성공이 아닙니다.
- 프로세스 출력의 보관 크기가 제한되어 있습니다. `output_lost`가 참이면 보관 창이 전체 로그가 아닙니다.
- 명령이 실행 중이면 같은 작업 공간에서 다른 명령이나 패치를 실행할 수 없습니다. 개발 서버를 켜 둔 채 같은 작업 공간을 수정하는 흐름에는 제약이 있습니다.
- 서버를 재시작하면 프로세스 핸들이 사라집니다. 패치 작업 기록은 데이터베이스 경로를 설정한 경우에만 유지됩니다.
- 컨테이너 실행과 완전한 OAuth 서버는 구현되어 있지 않습니다. 실제 ChatGPT 계정 연결도 아직 검증되지 않았습니다.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Then use the [Agent Loop integration guide](docs/agent-integration.md) for the r
| --- | --- |
| Inspect the environment and files | `workspace_info`, `find`, `read` |
| Apply a patch and retrieve its recorded state | `apply_patch`, `operation_status` |
| Run and control a process | `exec_command`, `read_process`, `write_stdin`, `terminate_process` |
| Run and control a process | `exec_command`, `read_process`, `process_status`, `write_stdin`, `terminate_process` |
| Track a logical job and queued user instructions | `work_open`, `steer_status`, `steer_claim_next`, `steer_complete`, `work_finish` |

Both transports expose the same tools. The HTTP `/inbox` API lets a user-facing client manage instruction drafts; it is a JSON API, not a browser inbox application.
Expand All @@ -33,8 +33,8 @@ CodeSpace reuses pinned Codex execution libraries for patches, terminal sessions

For agent integrations, account for these limits:

- Process results expose output and EOF, but no exit code. EOF alone cannot establish that a test passed.
- Process output is bounded; dropped output has no explicit flag in the MCP result.
- Judge process exit with `process_status`. EOF from `read_process` is not success.
- Process output is bounded. `output_lost` means the retained window is not the complete log.
- A live command blocks another command or patch in the same workspace. A development server cannot remain running while that workspace is patched.
- Process handles do not survive server restart. Patch-operation records persist only when a database path is configured.
- Container dispatch and a full OAuth server are not implemented. A live ChatGPT account connection remains unverified.
Expand Down
2 changes: 1 addition & 1 deletion crates/codex-runtime/tests/runtime_binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async fn hello_on_live_socket() {
match parsed.result {
Some(RunnerOpResult::Hello { protocol }) => {
assert_eq!(protocol, WIRE_PROTOCOL);
assert_eq!(protocol, 3);
assert_eq!(protocol, 4);
}
other => panic!("unexpected hello {other:?}"),
}
Expand Down
11 changes: 6 additions & 5 deletions crates/domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,18 @@ pub use patch::{
OperationEventName, OperationKind, OperationStatusParams, OperationStatusResult, PatchStatus,
};
pub use process::{
ExecCommandParams, ExecCommandResult, ExecDispatchStatus, ReadProcessParams, ReadProcessResult,
ExecCommandParams, ExecCommandResult, ExecDispatchStatus, ProcessState, ProcessStatusParams,
ProcessStatusResult, ProcessTermination, ReadProcessParams, ReadProcessResult,
TerminateProcessParams, WriteStdinParams,
};
pub use profile::Profile;
pub use tools::{
LIVE_TOOLS, SERVER_NAME, SERVER_VERSION, TOOL_APPLY_PATCH, TOOL_APPROVAL_CREATE,
TOOL_APPROVAL_RESOLVE, TOOL_EXEC_COMMAND, TOOL_FIND, TOOL_OPERATION_RESUME,
TOOL_OPERATION_STATUS, TOOL_READ, TOOL_READ_PROCESS, TOOL_STEER_CLAIM_NEXT,
TOOL_STEER_COMPLETE, TOOL_STEER_STATUS, TOOL_TERMINATE_PROCESS, TOOL_WORKSPACE_INFO,
TOOL_WORK_FINISH, TOOL_WORK_OPEN, TOOL_WRITE_STDIN, TRANSPORT_STDIO, TRANSPORT_STREAMABLE_HTTP,
W03_EXPOSED_TOOLS,
TOOL_OPERATION_STATUS, TOOL_PROCESS_STATUS, TOOL_READ, TOOL_READ_PROCESS,
TOOL_STEER_CLAIM_NEXT, TOOL_STEER_COMPLETE, TOOL_STEER_STATUS, TOOL_TERMINATE_PROCESS,
TOOL_WORKSPACE_INFO, TOOL_WORK_FINISH, TOOL_WORK_OPEN, TOOL_WRITE_STDIN, TRANSPORT_STDIO,
TRANSPORT_STREAMABLE_HTTP, W03_EXPOSED_TOOLS,
};
pub use work::{
ClaimedIntent, CoordinationHint, SteerClaimNextResult, SteerCompleteParams, SteerOutcome,
Expand Down
161 changes: 161 additions & 0 deletions crates/domain/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,63 @@ pub struct ReadProcessResult {
pub cursor: u64,
pub chunk: String,
pub eof: bool,
/// True when the retained window does not start at byte 0.
pub output_lost: bool,
/// First retained process-output offset (`dropped` in the runner).
pub retained_from: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coordination: Option<CoordinationHint>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProcessState {
Running,
Exited,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProcessTermination {
Exited,
Timeout,
Terminated,
Unknown,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProcessStatusParams {
pub process_id: ProcessId,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ProcessStatusResult {
pub process_id: ProcessId,
pub state: ProcessState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub termination: Option<ProcessTermination>,
pub output_total: u64,
pub output_retained_from: u64,
pub eof: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub coordination: Option<CoordinationHint>,
}

impl ProcessStatusResult {
pub fn invariants_hold(&self) -> bool {
match self.state {
ProcessState::Running => self.exit_code.is_none() && self.termination.is_none(),
ProcessState::Exited => match self.termination {
Some(ProcessTermination::Exited) => true,
Some(_) => self.exit_code.is_none(),
None => false,
},
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TerminateProcessParams {
pub process_id: ProcessId,
Expand Down Expand Up @@ -141,4 +194,112 @@ mod tests {
"result schema must include dispatch_status: {schema}"
);
}

#[test]
fn running_status_omits_exit_code() {
let json = serde_json::to_value(ProcessStatusResult {
process_id: ProcessId("proc-1".into()),
state: ProcessState::Running,
exit_code: None,
termination: None,
output_total: 0,
output_retained_from: 0,
eof: false,
coordination: None,
})
.unwrap();
assert_eq!(json["state"], "running");
assert!(json.get("exit_code").is_none());
assert!(json.get("termination").is_none());
assert!(ProcessStatusResult {
process_id: ProcessId("proc-1".into()),
state: ProcessState::Running,
exit_code: None,
termination: None,
output_total: 0,
output_retained_from: 0,
eof: false,
coordination: None,
}
.invariants_hold());
assert!(!ProcessStatusResult {
process_id: ProcessId("proc-1".into()),
state: ProcessState::Running,
exit_code: Some(0),
termination: None,
output_total: 0,
output_retained_from: 0,
eof: false,
coordination: None,
}
.invariants_hold());
let schema = serde_json::to_value(schemars::schema_for!(ProcessStatusResult)).unwrap();
let dumped = schema.to_string();
assert!(dumped.contains("output_total"), "{dumped}");
assert!(dumped.contains("termination"), "{dumped}");
}

#[test]
fn exited_success_may_include_exit_code() {
let json = serde_json::to_value(ProcessStatusResult {
process_id: ProcessId("proc-1".into()),
state: ProcessState::Exited,
exit_code: Some(0),
termination: Some(ProcessTermination::Exited),
output_total: 12,
output_retained_from: 0,
eof: true,
coordination: None,
})
.unwrap();
assert_eq!(json["state"], "exited");
assert_eq!(json["exit_code"], 0);
assert_eq!(json["termination"], "exited");
}

#[test]
fn timeout_status_has_no_exit_code() {
let json = serde_json::to_value(ProcessStatusResult {
process_id: ProcessId("proc-1".into()),
state: ProcessState::Exited,
exit_code: None,
termination: Some(ProcessTermination::Timeout),
output_total: 0,
output_retained_from: 0,
eof: true,
coordination: None,
})
.unwrap();
assert_eq!(json["termination"], "timeout");
assert!(json.get("exit_code").is_none());
}

#[test]
fn read_process_result_exposes_output_loss() {
let json = serde_json::to_value(ReadProcessResult {
process_id: ProcessId("proc-1".into()),
cursor: 10,
chunk: String::new(),
eof: false,
output_lost: true,
retained_from: 8,
coordination: None,
})
.unwrap();
assert_eq!(json["output_lost"], true);
assert_eq!(json["retained_from"], 8);
let schema = serde_json::to_value(schemars::schema_for!(ReadProcessResult)).unwrap();
let dumped = schema.to_string();
assert!(dumped.contains("output_lost"), "{dumped}");
assert!(dumped.contains("retained_from"), "{dumped}");
}

#[test]
fn process_status_params_are_process_id_only() {
let schema = serde_json::to_value(schemars::schema_for!(ProcessStatusParams)).unwrap();
let dumped = schema.to_string();
assert!(dumped.contains("process_id"), "{dumped}");
assert!(!dumped.contains("tty_size"), "{dumped}");
assert!(!dumped.contains("signal"), "{dumped}");
}
}
2 changes: 2 additions & 0 deletions crates/domain/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub const TOOL_APPLY_PATCH: &str = "apply_patch";
pub const TOOL_EXEC_COMMAND: &str = "exec_command";
pub const TOOL_WRITE_STDIN: &str = "write_stdin";
pub const TOOL_READ_PROCESS: &str = "read_process";
pub const TOOL_PROCESS_STATUS: &str = "process_status";
pub const TOOL_TERMINATE_PROCESS: &str = "terminate_process";
pub const TOOL_OPERATION_STATUS: &str = "operation_status";
pub const TOOL_WORK_OPEN: &str = "work_open";
Expand All @@ -33,6 +34,7 @@ pub const LIVE_TOOLS: &[&str] = &[
TOOL_EXEC_COMMAND,
TOOL_WRITE_STDIN,
TOOL_READ_PROCESS,
TOOL_PROCESS_STATUS,
TOOL_TERMINATE_PROCESS,
TOOL_WORK_OPEN,
TOOL_STEER_STATUS,
Expand Down
22 changes: 21 additions & 1 deletion crates/runner/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
use std::collections::BTreeMap;
use std::path::Path;

use codespace_domain::{ErrorBody, ErrorCode, FileChange, PatchStatus, ProcessId, Profile};
use codespace_domain::{
ErrorBody, ErrorCode, FileChange, PatchStatus, ProcessId, ProcessState, ProcessTermination,
Profile,
};
use codespace_policy::NetworkAxis;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -113,6 +116,23 @@ pub struct RunnerReadResult {
pub cursor: u64,
pub chunk: String,
pub eof: bool,
#[serde(default)]
pub output_lost: bool,
#[serde(default)]
pub retained_from: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnerProcessStatus {
pub process_id: ProcessId,
pub state: ProcessState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub termination: Option<ProcessTermination>,
pub output_total: u64,
pub output_retained_from: u64,
pub eof: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
Expand Down
26 changes: 24 additions & 2 deletions crates/runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ mod wire;
pub use api::{
default_exec_timeout_ms, runner_local_exec_env, RunnerApplyPatchRequest,
RunnerApplyPatchResult, RunnerCwd, RunnerError, RunnerExecEnv, RunnerExecPolicy,
RunnerExecRequest, RunnerExecResult, RunnerReadProcess, RunnerReadResult, RunnerWriteStdin,
DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES,
RunnerExecRequest, RunnerExecResult, RunnerProcessStatus, RunnerReadProcess, RunnerReadResult,
RunnerWriteStdin, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES,
};
pub use files::{DEFAULT_FIND_LIMIT, DEFAULT_READ_LIMIT, VERSION_ABSENT};
pub use patch_helper::ensure_helper_for_tests;
Expand Down Expand Up @@ -178,6 +178,10 @@ pub trait Runner: Send + Sync {
&self,
req: RunnerReadProcess,
) -> impl std::future::Future<Output = Result<RunnerReadResult, RunnerError>> + Send;
fn process_status(
&self,
process_id: &ProcessId,
) -> impl std::future::Future<Output = Result<RunnerProcessStatus, RunnerError>> + Send;
fn terminate(
&self,
process_id: &ProcessId,
Expand Down Expand Up @@ -231,6 +235,14 @@ impl Runner for InProcessRunner {
self.read_host_process(req).map_err(RunnerError::from)
}

async fn process_status(
&self,
process_id: &ProcessId,
) -> Result<RunnerProcessStatus, RunnerError> {
self.host_process_status(process_id)
.map_err(RunnerError::from)
}

async fn terminate(&self, process_id: &ProcessId) -> Result<(), RunnerError> {
self.kill_host(process_id).map_err(RunnerError::from)
}
Expand Down Expand Up @@ -315,6 +327,16 @@ impl Runner for RuntimeBackend {
}
}

async fn process_status(
&self,
process_id: &ProcessId,
) -> Result<RunnerProcessStatus, RunnerError> {
match self {
Self::InProcess(runner) => runner.process_status(process_id).await,
Self::Uds(runner) => runner.process_status(process_id).await,
}
}

async fn terminate(&self, process_id: &ProcessId) -> Result<(), RunnerError> {
match self {
Self::InProcess(runner) => runner.terminate(process_id).await,
Expand Down
Loading
Loading