mirror of
https://github.com/openai/codex.git
synced 2026-03-03 13:13:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7cf04363a |
11
.github/workflows/bazel.yml
vendored
11
.github/workflows/bazel.yml
vendored
@@ -28,17 +28,14 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
# Linux
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
- os: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
# 2026-02-27 Bazel tests have been flaky on arm in CI.
|
||||
# Disable until we can investigate and stabilize them.
|
||||
# - os: ubuntu-24.04-arm
|
||||
# target: aarch64-unknown-linux-musl
|
||||
# - os: ubuntu-24.04-arm
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
|
||||
# TODO: Enable Windows once we fix the toolchain issues there.
|
||||
#- os: windows-latest
|
||||
# target: x86_64-pc-windows-gnullvm
|
||||
|
||||
109
.github/workflows/issue-deduplicator.yml
vendored
109
.github/workflows/issue-deduplicator.yml
vendored
@@ -7,17 +7,15 @@ on:
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
gather-duplicates-all:
|
||||
name: Identify potential duplicates (all issues)
|
||||
gather-duplicates:
|
||||
name: Identify potential duplicates
|
||||
# Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
|
||||
if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
issues_json: ${{ steps.normalize-all.outputs.issues_json }}
|
||||
reason: ${{ steps.normalize-all.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
codex_output: ${{ steps.select-final.outputs.codex_output }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -31,6 +29,7 @@ jobs:
|
||||
|
||||
CURRENT_ISSUE_FILE=codex-current-issue.json
|
||||
EXISTING_ALL_FILE=codex-existing-issues-all.json
|
||||
EXISTING_OPEN_FILE=codex-existing-issues-open.json
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
@@ -48,6 +47,22 @@ jobs:
|
||||
}]' \
|
||||
> "$EXISTING_ALL_FILE"
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
--limit 1000 \
|
||||
--state open \
|
||||
--search "sort:created-desc" \
|
||||
| jq '[.[] | {
|
||||
number,
|
||||
title,
|
||||
body: ((.body // "")[0:4000]),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
state,
|
||||
labels: ((.labels // []) | map(.name))
|
||||
}]' \
|
||||
> "$EXISTING_OPEN_FILE"
|
||||
|
||||
gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
--json number,title,body \
|
||||
@@ -56,6 +71,7 @@ jobs:
|
||||
|
||||
echo "Prepared duplicate detection input files."
|
||||
echo "all_issue_count=$(jq 'length' "$EXISTING_ALL_FILE")"
|
||||
echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"
|
||||
|
||||
# Prompt instructions are intentionally inline in this workflow. The old
|
||||
# .github/prompts/issue-deduplicator.txt file is obsolete and removed.
|
||||
@@ -142,59 +158,9 @@ jobs:
|
||||
echo "has_matches=$has_matches"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
gather-duplicates-open:
|
||||
name: Identify potential duplicates (open issues fallback)
|
||||
# Pass 1 may drop sudo on the runner, so run the fallback in a fresh job.
|
||||
needs: gather-duplicates-all
|
||||
if: ${{ needs.gather-duplicates-all.result == 'success' && needs.gather-duplicates-all.outputs.has_matches != 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
issues_json: ${{ steps.normalize-open.outputs.issues_json }}
|
||||
reason: ${{ steps.normalize-open.outputs.reason }}
|
||||
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare Codex inputs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
CURRENT_ISSUE_FILE=codex-current-issue.json
|
||||
EXISTING_OPEN_FILE=codex-existing-issues-open.json
|
||||
|
||||
gh issue list --repo "$REPO" \
|
||||
--json number,title,body,createdAt,updatedAt,state,labels \
|
||||
--limit 1000 \
|
||||
--state open \
|
||||
--search "sort:created-desc" \
|
||||
| jq '[.[] | {
|
||||
number,
|
||||
title,
|
||||
body: ((.body // "")[0:4000]),
|
||||
createdAt,
|
||||
updatedAt,
|
||||
state,
|
||||
labels: ((.labels // []) | map(.name))
|
||||
}]' \
|
||||
> "$EXISTING_OPEN_FILE"
|
||||
|
||||
gh issue view "$ISSUE_NUMBER" \
|
||||
--repo "$REPO" \
|
||||
--json number,title,body \
|
||||
| jq '{number, title, body: ((.body // "")[0:4000])}' \
|
||||
> "$CURRENT_ISSUE_FILE"
|
||||
|
||||
echo "Prepared fallback duplicate detection input files."
|
||||
echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"
|
||||
|
||||
- id: codex-open
|
||||
name: Find duplicates (pass 2, open issues)
|
||||
if: ${{ steps.normalize-all.outputs.has_matches != 'true' }}
|
||||
uses: openai/codex-action@main
|
||||
with:
|
||||
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
|
||||
@@ -234,6 +200,7 @@ jobs:
|
||||
|
||||
- id: normalize-open
|
||||
name: Normalize pass 2 output
|
||||
if: ${{ steps.normalize-all.outputs.has_matches != 'true' }}
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ steps.codex-open.outputs.final-message }}
|
||||
CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
@@ -276,27 +243,15 @@ jobs:
|
||||
echo "has_matches=$has_matches"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
select-final:
|
||||
name: Select final duplicate set
|
||||
needs:
|
||||
- gather-duplicates-all
|
||||
- gather-duplicates-open
|
||||
if: ${{ always() && needs.gather-duplicates-all.result == 'success' && (needs.gather-duplicates-open.result == 'success' || needs.gather-duplicates-open.result == 'skipped') }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
codex_output: ${{ steps.select-final.outputs.codex_output }}
|
||||
steps:
|
||||
- id: select-final
|
||||
name: Select final duplicate set
|
||||
env:
|
||||
PASS1_ISSUES: ${{ needs.gather-duplicates-all.outputs.issues_json }}
|
||||
PASS1_REASON: ${{ needs.gather-duplicates-all.outputs.reason }}
|
||||
PASS2_ISSUES: ${{ needs.gather-duplicates-open.outputs.issues_json }}
|
||||
PASS2_REASON: ${{ needs.gather-duplicates-open.outputs.reason }}
|
||||
PASS1_HAS_MATCHES: ${{ needs.gather-duplicates-all.outputs.has_matches }}
|
||||
PASS2_HAS_MATCHES: ${{ needs.gather-duplicates-open.outputs.has_matches }}
|
||||
PASS1_ISSUES: ${{ steps.normalize-all.outputs.issues_json }}
|
||||
PASS1_REASON: ${{ steps.normalize-all.outputs.reason }}
|
||||
PASS2_ISSUES: ${{ steps.normalize-open.outputs.issues_json }}
|
||||
PASS2_REASON: ${{ steps.normalize-open.outputs.reason }}
|
||||
PASS1_HAS_MATCHES: ${{ steps.normalize-all.outputs.has_matches }}
|
||||
PASS2_HAS_MATCHES: ${{ steps.normalize-open.outputs.has_matches }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
@@ -334,8 +289,8 @@ jobs:
|
||||
|
||||
comment-on-issue:
|
||||
name: Comment with potential duplicates
|
||||
needs: select-final
|
||||
if: ${{ needs.select-final.result != 'skipped' }}
|
||||
needs: gather-duplicates
|
||||
if: ${{ needs.gather-duplicates.result != 'skipped' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -344,7 +299,7 @@ jobs:
|
||||
- name: Comment on issue
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
|
||||
CODEX_OUTPUT: ${{ needs.gather-duplicates.outputs.codex_output }}
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/shell-tool-mcp.yml
vendored
6
.github/workflows/shell-tool-mcp.yml
vendored
@@ -146,8 +146,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone https://git.savannah.gnu.org/git/bash /tmp/bash
|
||||
git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
@@ -187,8 +188,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone https://git.savannah.gnu.org/git/bash /tmp/bash
|
||||
git clone --depth 1 https://github.com/bolinfest/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
|
||||
41
MODULE.bazel.lock
generated
41
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
651
codex-rs/Cargo.lock
generated
651
codex-rs/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,6 @@ members = [
|
||||
"mcp-server",
|
||||
"network-proxy",
|
||||
"ollama",
|
||||
"artifact-presentation",
|
||||
"artifact-spreadsheet",
|
||||
"process-hardening",
|
||||
"protocol",
|
||||
"rmcp-client",
|
||||
@@ -111,8 +109,6 @@ codex-mcp-server = { path = "mcp-server" }
|
||||
codex-network-proxy = { path = "network-proxy" }
|
||||
codex-ollama = { path = "ollama" }
|
||||
codex-otel = { path = "otel" }
|
||||
codex-artifact-presentation = { path = "artifact-presentation" }
|
||||
codex-artifact-spreadsheet = { path = "artifact-spreadsheet" }
|
||||
codex-process-hardening = { path = "process-hardening" }
|
||||
codex-protocol = { path = "protocol" }
|
||||
codex-responses-api-proxy = { path = "responses-api-proxy" }
|
||||
@@ -219,7 +215,6 @@ owo-colors = "4.3.0"
|
||||
path-absolutize = "3.1.1"
|
||||
pathdiff = "0.2"
|
||||
portable-pty = "0.9.0"
|
||||
ppt-rs = "0.2.6"
|
||||
predicates = "3"
|
||||
pretty_assertions = "1.4.1"
|
||||
pulldown-cmark = "0.10"
|
||||
@@ -354,7 +349,6 @@ ignored = [
|
||||
"openssl-sys",
|
||||
"codex-utils-readiness",
|
||||
"codex-secrets",
|
||||
"codex-artifact-spreadsheet"
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -20,7 +20,6 @@ codex-utils-absolute-path = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -1703,12 +1703,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Settings for a collaboration mode.",
|
||||
"properties": {
|
||||
@@ -1939,23 +1933,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2178,23 +2155,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2339,23 +2299,6 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -2466,24 +2409,6 @@
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1138,16 +1138,6 @@
|
||||
],
|
||||
"description": "How to sandbox commands executed in the system"
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
},
|
||||
@@ -4244,14 +4234,8 @@
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"SessionUpdated": {
|
||||
"SessionCreated": {
|
||||
"properties": {
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -4262,6 +4246,27 @@
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SessionCreated"
|
||||
],
|
||||
"title": "SessionCreatedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"SessionUpdated": {
|
||||
"properties": {
|
||||
"backend_prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SessionUpdated"
|
||||
],
|
||||
@@ -4292,40 +4297,6 @@
|
||||
"title": "ConversationItemAddedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ConversationItemDone": {
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"item_id"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConversationItemDone"
|
||||
],
|
||||
"title": "ConversationItemDoneRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"HandoffRequested": {
|
||||
"$ref": "#/definitions/RealtimeHandoffRequested"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"HandoffRequested"
|
||||
],
|
||||
"title": "HandoffRequestedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -4341,47 +4312,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RealtimeHandoffMessage": {
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RealtimeHandoffRequested": {
|
||||
"properties": {
|
||||
"handoff_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_transcript": {
|
||||
"type": "string"
|
||||
},
|
||||
"item_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RealtimeHandoffMessage"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"handoff_id",
|
||||
"input_transcript",
|
||||
"item_id",
|
||||
"messages"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
@@ -5420,12 +5350,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionNetworkProxyRuntime": {
|
||||
"properties": {
|
||||
"admin_addr": {
|
||||
@@ -6770,16 +6694,6 @@
|
||||
],
|
||||
"description": "How to sandbox commands executed in the system"
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"$ref": "#/definitions/ThreadId"
|
||||
},
|
||||
|
||||
@@ -70,18 +70,7 @@
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true,
|
||||
"trace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/W3cTraceContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional W3C Trace Context for distributed tracing."
|
||||
}
|
||||
"params": true
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
@@ -113,23 +102,6 @@
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"W3cTraceContext": {
|
||||
"properties": {
|
||||
"traceparent": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent.",
|
||||
|
||||
@@ -11,23 +11,6 @@
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"W3cTraceContext": {
|
||||
"properties": {
|
||||
"traceparent": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "A request that expects a response.",
|
||||
@@ -38,18 +21,7 @@
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true,
|
||||
"trace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/W3cTraceContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional W3C Trace Context for distributed tracing."
|
||||
}
|
||||
"params": true
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
|
||||
@@ -46,16 +46,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PlanType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -1432,32 +1422,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServerRequestResolvedNotification": {
|
||||
"properties": {
|
||||
"requestId": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requestId",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1665,10 +1629,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1738,7 +1698,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
@@ -3463,26 +3422,6 @@
|
||||
"title": "Item/fileChange/outputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"serverRequest/resolved"
|
||||
],
|
||||
"title": "ServerRequest/resolvedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ServerRequestResolvedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ServerRequest/resolvedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
|
||||
@@ -357,7 +357,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -382,7 +382,7 @@
|
||||
"description": "NEW APIs",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -406,7 +406,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -430,7 +430,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -454,7 +454,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -478,7 +478,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -502,7 +502,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -526,7 +526,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -550,7 +550,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -574,7 +574,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -598,7 +598,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -622,7 +622,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -646,7 +646,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -670,7 +670,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -694,7 +694,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -718,7 +718,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -742,7 +742,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -766,7 +766,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -790,7 +790,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -814,7 +814,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -838,7 +838,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -862,7 +862,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -886,7 +886,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -910,7 +910,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -934,7 +934,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -958,7 +958,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -981,7 +981,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1005,7 +1005,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1029,7 +1029,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1053,7 +1053,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1077,7 +1077,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1100,7 +1100,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1123,7 +1123,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1148,7 +1148,7 @@
|
||||
"description": "Execute a command (argv vector) under the server's sandbox.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1172,7 +1172,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1196,7 +1196,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1220,7 +1220,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1244,7 +1244,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1268,7 +1268,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1292,7 +1292,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1315,7 +1315,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -1339,7 +1339,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -2306,16 +2306,6 @@
|
||||
],
|
||||
"description": "How to sandbox commands executed in the system"
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"$ref": "#/definitions/v2/ThreadId"
|
||||
},
|
||||
@@ -3075,7 +3065,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
@@ -4953,7 +4943,7 @@
|
||||
"$ref": "#/definitions/JSONRPCErrorError"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5021,23 +5011,12 @@
|
||||
"description": "A request that expects a response.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": true,
|
||||
"trace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/W3cTraceContext"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional W3C Trace Context for distributed tracing."
|
||||
}
|
||||
"params": true
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
@@ -5051,7 +5030,7 @@
|
||||
"description": "A successful (non-error) response to a request.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"result": true
|
||||
},
|
||||
@@ -5464,14 +5443,8 @@
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"SessionUpdated": {
|
||||
"SessionCreated": {
|
||||
"properties": {
|
||||
"instructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"session_id": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -5482,6 +5455,27 @@
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SessionCreated"
|
||||
],
|
||||
"title": "SessionCreatedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"SessionUpdated": {
|
||||
"properties": {
|
||||
"backend_prompt": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"SessionUpdated"
|
||||
],
|
||||
@@ -5512,40 +5506,6 @@
|
||||
"title": "ConversationItemAddedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ConversationItemDone": {
|
||||
"properties": {
|
||||
"item_id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"item_id"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConversationItemDone"
|
||||
],
|
||||
"title": "ConversationItemDoneRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"HandoffRequested": {
|
||||
"$ref": "#/definitions/RealtimeHandoffRequested"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"HandoffRequested"
|
||||
],
|
||||
"title": "HandoffRequestedRealtimeEvent",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -5561,47 +5521,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RealtimeHandoffMessage": {
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RealtimeHandoffRequested": {
|
||||
"properties": {
|
||||
"handoff_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_transcript": {
|
||||
"type": "string"
|
||||
},
|
||||
"item_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RealtimeHandoffMessage"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"handoff_id",
|
||||
"input_transcript",
|
||||
"item_id",
|
||||
"messages"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RejectConfig": {
|
||||
"properties": {
|
||||
"mcp_elicitations": {
|
||||
@@ -5625,7 +5544,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
@@ -5635,7 +5553,7 @@
|
||||
"type": "integer"
|
||||
}
|
||||
],
|
||||
"title": "RequestId"
|
||||
"description": "ID of a request, which can be either a string or an integer."
|
||||
},
|
||||
"RequestUserInputQuestion": {
|
||||
"properties": {
|
||||
@@ -6276,26 +6194,6 @@
|
||||
"title": "Item/fileChange/outputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
"enum": [
|
||||
"serverRequest/resolved"
|
||||
],
|
||||
"title": "ServerRequest/resolvedNotificationMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ServerRequestResolvedNotification"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "ServerRequest/resolvedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"method": {
|
||||
@@ -6749,7 +6647,7 @@
|
||||
"description": "NEW APIs Sent when approval is requested for a specific command execution. This request is used for Turns started via turn/start.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6774,7 +6672,7 @@
|
||||
"description": "Sent when approval is requested for a specific file change. This request is used for Turns started via turn/start.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6799,7 +6697,7 @@
|
||||
"description": "EXPERIMENTAL - Request input from the user for a tool call.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6824,7 +6722,7 @@
|
||||
"description": "Execute a dynamic tool call on the client.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6848,7 +6746,7 @@
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6873,7 +6771,7 @@
|
||||
"description": "DEPRECATED APIs below Request to approve a patch. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -6898,7 +6796,7 @@
|
||||
"description": "Request to exec a command. This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).",
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
@@ -7301,23 +7199,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"W3cTraceContext": {
|
||||
"properties": {
|
||||
"traceparent": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tracestate": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"v2": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
@@ -7417,16 +7298,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/PlanType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
@@ -8605,16 +8476,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -10865,16 +10726,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -11259,17 +11110,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ResidencyRequirement": {
|
||||
"enum": [
|
||||
"us"
|
||||
@@ -12009,29 +11849,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ServerRequestResolvedNotification": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"requestId": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requestId",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ServerRequestResolvedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -12632,10 +12449,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -12705,7 +12518,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
@@ -12847,23 +12659,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -12902,16 +12697,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/v2/Thread"
|
||||
}
|
||||
@@ -13857,23 +13642,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -13912,16 +13680,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/v2/Thread"
|
||||
}
|
||||
@@ -14100,23 +13858,6 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
@@ -14150,16 +13891,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/v2/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/v2/Thread"
|
||||
}
|
||||
@@ -14727,24 +14458,6 @@
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -26,20 +26,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PlanType": {
|
||||
"enum": [
|
||||
"free",
|
||||
"go",
|
||||
"plus",
|
||||
"pro",
|
||||
"team",
|
||||
"business",
|
||||
"enterprise",
|
||||
"edu",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -52,16 +38,6 @@
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"planType": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/PlanType"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "AccountUpdatedNotification",
|
||||
|
||||
@@ -323,16 +323,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -618,16 +608,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"web_search": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -705,12 +685,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ToolsV2": {
|
||||
"properties": {
|
||||
"view_image": {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"requestId": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"requestId",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ServerRequestResolvedNotification",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -50,12 +50,6 @@
|
||||
"danger-full-access"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using path, the thread_id param will be ignored.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -118,23 +112,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -738,12 +738,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -888,10 +882,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -961,7 +951,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
@@ -1912,16 +1901,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/Thread"
|
||||
}
|
||||
|
||||
@@ -655,10 +655,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -728,7 +724,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -655,10 +655,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -728,7 +724,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -738,12 +738,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WebSearchAction": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -916,23 +910,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -738,12 +738,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -888,10 +882,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -961,7 +951,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
@@ -1912,16 +1901,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/Thread"
|
||||
}
|
||||
|
||||
@@ -655,10 +655,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -728,7 +724,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -75,12 +75,6 @@
|
||||
"danger-full-access"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@@ -162,23 +156,6 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "ThreadStartParams",
|
||||
|
||||
@@ -738,12 +738,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -888,10 +882,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -961,7 +951,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
@@ -1912,16 +1901,6 @@
|
||||
"sandbox": {
|
||||
"$ref": "#/definitions/SandboxPolicy"
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"thread": {
|
||||
"$ref": "#/definitions/Thread"
|
||||
}
|
||||
|
||||
@@ -655,10 +655,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -728,7 +724,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -655,10 +655,6 @@
|
||||
"description": "Working directory captured for the thread.",
|
||||
"type": "string"
|
||||
},
|
||||
"ephemeral": {
|
||||
"description": "Whether the thread is ephemeral and should not be materialized on disk.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"gitInfo": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -728,7 +724,6 @@
|
||||
"cliVersion",
|
||||
"createdAt",
|
||||
"cwd",
|
||||
"ephemeral",
|
||||
"id",
|
||||
"modelProvider",
|
||||
"preview",
|
||||
|
||||
@@ -299,12 +299,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ServiceTier": {
|
||||
"enum": [
|
||||
"fast"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Settings": {
|
||||
"description": "Settings for a collaboration mode.",
|
||||
"properties": {
|
||||
@@ -545,24 +539,6 @@
|
||||
],
|
||||
"description": "Override the sandbox policy for this turn and subsequent turns."
|
||||
},
|
||||
"serviceTier": {
|
||||
"anyOf": [
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ServiceTier"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the service tier for this turn and subsequent turns."
|
||||
},
|
||||
"summary": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RealtimeAudioFrame } from "./RealtimeAudioFrame";
|
||||
import type { RealtimeHandoffRequested } from "./RealtimeHandoffRequested";
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
export type RealtimeEvent = { "SessionUpdated": { session_id: string, instructions: string | null, } } | { "AudioOut": RealtimeAudioFrame } | { "ConversationItemAdded": JsonValue } | { "ConversationItemDone": { item_id: string, } } | { "HandoffRequested": RealtimeHandoffRequested } | { "Error": string };
|
||||
export type RealtimeEvent = { "SessionCreated": { session_id: string, } } | { "SessionUpdated": { backend_prompt: string | null, } } | { "AudioOut": RealtimeAudioFrame } | { "ConversationItemAdded": JsonValue } | { "Error": string };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RealtimeHandoffMessage = { role: string, text: string, };
|
||||
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RealtimeHandoffMessage } from "./RealtimeHandoffMessage";
|
||||
|
||||
export type RealtimeHandoffRequested = { handoff_id: string, item_id: string, input_transcript: string, messages: Array<RealtimeHandoffMessage>, };
|
||||
@@ -6,11 +6,10 @@ import type { InputItem } from "./InputItem";
|
||||
import type { ReasoningEffort } from "./ReasoningEffort";
|
||||
import type { ReasoningSummary } from "./ReasoningSummary";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { ServiceTier } from "./ServiceTier";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
|
||||
export type SendUserTurnParams = { conversationId: ThreadId, items: Array<InputItem>, cwd: string, approvalPolicy: AskForApproval, sandboxPolicy: SandboxPolicy, model: string, serviceTier?: ServiceTier | null | null, effort: ReasoningEffort | null, summary: ReasoningSummary,
|
||||
export type SendUserTurnParams = { conversationId: ThreadId, items: Array<InputItem>, cwd: string, approvalPolicy: AskForApproval, sandboxPolicy: SandboxPolicy, model: string, effort: ReasoningEffort | null, summary: ReasoningSummary,
|
||||
/**
|
||||
* Optional JSON Schema used to constrain the final assistant message for this turn.
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,6 @@ import type { RawResponseItemCompletedNotification } from "./v2/RawResponseItemC
|
||||
import type { ReasoningSummaryPartAddedNotification } from "./v2/ReasoningSummaryPartAddedNotification";
|
||||
import type { ReasoningSummaryTextDeltaNotification } from "./v2/ReasoningSummaryTextDeltaNotification";
|
||||
import type { ReasoningTextDeltaNotification } from "./v2/ReasoningTextDeltaNotification";
|
||||
import type { ServerRequestResolvedNotification } from "./v2/ServerRequestResolvedNotification";
|
||||
import type { TerminalInteractionNotification } from "./v2/TerminalInteractionNotification";
|
||||
import type { ThreadArchivedNotification } from "./v2/ThreadArchivedNotification";
|
||||
import type { ThreadClosedNotification } from "./v2/ThreadClosedNotification";
|
||||
@@ -51,4 +50,4 @@ import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldW
|
||||
/**
|
||||
* Notification sent from the server to the client.
|
||||
*/
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
|
||||
export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification } | { "method": "authStatusChange", "params": AuthStatusChangeNotification } | { "method": "loginChatGptComplete", "params": LoginChatGptCompleteNotification } | { "method": "sessionConfigured", "params": SessionConfiguredNotification };
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ServiceTier = "fast";
|
||||
@@ -5,7 +5,6 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { EventMsg } from "./EventMsg";
|
||||
import type { ReasoningEffort } from "./ReasoningEffort";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { ServiceTier } from "./ServiceTier";
|
||||
import type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
@@ -17,7 +16,7 @@ thread_name?: string,
|
||||
/**
|
||||
* Tell the client what model is being queried.
|
||||
*/
|
||||
model: string, model_provider_id: string, service_tier: ServiceTier | null,
|
||||
model: string, model_provider_id: string,
|
||||
/**
|
||||
* When to escalate for approval for execution
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { EventMsg } from "./EventMsg";
|
||||
import type { ReasoningEffort } from "./ReasoningEffort";
|
||||
import type { ServiceTier } from "./ServiceTier";
|
||||
import type { ThreadId } from "./ThreadId";
|
||||
|
||||
export type SessionConfiguredNotification = { sessionId: ThreadId, model: string, serviceTier: ServiceTier | null, reasoningEffort: ReasoningEffort | null, historyLogId: bigint, historyEntryCount: number, initialMessages: Array<EventMsg> | null, rolloutPath: string, };
|
||||
export type SessionConfiguredNotification = { sessionId: ThreadId, model: string, reasoningEffort: ReasoningEffort | null, historyLogId: bigint, historyEntryCount: number, initialMessages: Array<EventMsg> | null, rolloutPath: string, };
|
||||
|
||||
@@ -161,8 +161,6 @@ export type { RealtimeConversationClosedEvent } from "./RealtimeConversationClos
|
||||
export type { RealtimeConversationRealtimeEvent } from "./RealtimeConversationRealtimeEvent";
|
||||
export type { RealtimeConversationStartedEvent } from "./RealtimeConversationStartedEvent";
|
||||
export type { RealtimeEvent } from "./RealtimeEvent";
|
||||
export type { RealtimeHandoffMessage } from "./RealtimeHandoffMessage";
|
||||
export type { RealtimeHandoffRequested } from "./RealtimeHandoffRequested";
|
||||
export type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent";
|
||||
export type { ReasoningEffort } from "./ReasoningEffort";
|
||||
export type { ReasoningItem } from "./ReasoningItem";
|
||||
@@ -200,7 +198,6 @@ export type { SendUserTurnParams } from "./SendUserTurnParams";
|
||||
export type { SendUserTurnResponse } from "./SendUserTurnResponse";
|
||||
export type { ServerNotification } from "./ServerNotification";
|
||||
export type { ServerRequest } from "./ServerRequest";
|
||||
export type { ServiceTier } from "./ServiceTier";
|
||||
export type { SessionConfiguredEvent } from "./SessionConfiguredEvent";
|
||||
export type { SessionConfiguredNotification } from "./SessionConfiguredNotification";
|
||||
export type { SessionNetworkProxyRuntime } from "./SessionNetworkProxyRuntime";
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthMode } from "../AuthMode";
|
||||
import type { PlanType } from "../PlanType";
|
||||
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, planType: PlanType | null, };
|
||||
export type AccountUpdatedNotification = { authMode: AuthMode | null, };
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import type { ForcedLoginMethod } from "../ForcedLoginMethod";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { Verbosity } from "../Verbosity";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
@@ -15,4 +14,4 @@ import type { SandboxMode } from "./SandboxMode";
|
||||
import type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
|
||||
import type { ToolsV2 } from "./ToolsV2";
|
||||
|
||||
export type Config = {model: string | null, review_model: string | null, model_context_window: bigint | null, model_auto_compact_token_limit: bigint | null, model_provider: string | null, approval_policy: AskForApproval | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, service_tier: ServiceTier | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
export type Config = {model: string | null, review_model: string | null, model_context_window: bigint | null, model_auto_compact_token_limit: bigint | null, model_provider: string | null, approval_policy: AskForApproval | null, sandbox_mode: SandboxMode | null, sandbox_workspace_write: SandboxWorkspaceWrite | null, forced_chatgpt_workspace_id: string | null, forced_login_method: ForcedLoginMethod | null, web_search: WebSearchMode | null, tools: ToolsV2 | null, profile: string | null, profiles: { [key in string]?: ProfileV2 }, instructions: string | null, developer_instructions: string | null, compact_prompt: string | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, analytics: AnalyticsConfig | null} & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { Verbosity } from "../Verbosity";
|
||||
import type { WebSearchMode } from "../WebSearchMode";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
|
||||
export type ProfileV2 = { model: string | null, model_provider: string | null, approval_policy: AskForApproval | null, service_tier: ServiceTier | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, chatgpt_base_url: string | null, } & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
export type ProfileV2 = { model: string | null, model_provider: string | null, approval_policy: AskForApproval | null, model_reasoning_effort: ReasoningEffort | null, model_reasoning_summary: ReasoningSummary | null, model_verbosity: Verbosity | null, web_search: WebSearchMode | null, chatgpt_base_url: string | null, } & ({ [key in string]?: number | string | boolean | Array<JsonValue> | { [key in string]?: JsonValue } | null });
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RequestId } from "../RequestId";
|
||||
|
||||
export type ServerRequestResolvedNotification = { threadId: string, requestId: RequestId, };
|
||||
@@ -11,10 +11,6 @@ export type Thread = { id: string,
|
||||
* Usually the first user message in the thread, if available.
|
||||
*/
|
||||
preview: string,
|
||||
/**
|
||||
* Whether the thread is ephemeral and should not be materialized on disk.
|
||||
*/
|
||||
ephemeral: boolean,
|
||||
/**
|
||||
* Model provider used for this thread (for example, 'openai').
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
@@ -22,7 +21,7 @@ export type ThreadForkParams = {threadId: string, /**
|
||||
path?: string | null, /**
|
||||
* Configuration overrides for the forked thread, if any.
|
||||
*/
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
|
||||
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ResponseItem } from "../ResponseItem";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
@@ -31,7 +30,7 @@ history?: Array<ResponseItem> | null, /**
|
||||
path?: string | null, /**
|
||||
* Configuration overrides for the resumed thread, if any.
|
||||
*/
|
||||
model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /**
|
||||
* If true, persist additional rollout EventMsg variants required to
|
||||
* reconstruct a richer thread history on subsequent resume/fork/read.
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxMode } from "./SandboxMode";
|
||||
|
||||
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
|
||||
export type ThreadStartParams = {model?: string | null, modelProvider?: string | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, /**
|
||||
* If true, opt into emitting raw Responses API items on the event stream.
|
||||
* This is for internal use only (e.g. Codex Cloud).
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, cwd: string, approvalPolicy: AskForApproval, sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null, };
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { CollaborationMode } from "../CollaborationMode";
|
||||
import type { Personality } from "../Personality";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { ReasoningSummary } from "../ReasoningSummary";
|
||||
import type { ServiceTier } from "../ServiceTier";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
@@ -24,9 +23,6 @@ sandboxPolicy?: SandboxPolicy | null, /**
|
||||
* Override the model for this turn and subsequent turns.
|
||||
*/
|
||||
model?: string | null, /**
|
||||
* Override the service tier for this turn and subsequent turns.
|
||||
*/
|
||||
serviceTier?: ServiceTier | null | null, /**
|
||||
* Override the reasoning effort for this turn and subsequent turns.
|
||||
*/
|
||||
effort?: ReasoningEffort | null, /**
|
||||
|
||||
@@ -142,7 +142,6 @@ export type { ReviewTarget } from "./ReviewTarget";
|
||||
export type { SandboxMode } from "./SandboxMode";
|
||||
export type { SandboxPolicy } from "./SandboxPolicy";
|
||||
export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite";
|
||||
export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification";
|
||||
export type { SessionSource } from "./SessionSource";
|
||||
export type { SkillDependencies } from "./SkillDependencies";
|
||||
export type { SkillErrorInfo } from "./SkillErrorInfo";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! We do not do true JSON-RPC 2.0, as we neither send nor expect the
|
||||
//! "jsonrpc": "2.0" field.
|
||||
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -9,9 +8,7 @@ use ts_rs::TS;
|
||||
|
||||
pub const JSONRPC_VERSION: &str = "2.0";
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, PartialOrd, Ord, Deserialize, Serialize, Hash, Eq, JsonSchema, TS,
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, JsonSchema, TS)]
|
||||
#[serde(untagged)]
|
||||
pub enum RequestId {
|
||||
String(String),
|
||||
@@ -39,10 +36,6 @@ pub struct JSONRPCRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub params: Option<serde_json::Value>,
|
||||
/// Optional W3C Trace Context for distributed tracing.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
}
|
||||
|
||||
/// A notification which does not expect a response.
|
||||
|
||||
@@ -548,14 +548,6 @@ macro_rules! server_request_definitions {
|
||||
)*
|
||||
}
|
||||
|
||||
impl ServerRequest {
|
||||
pub fn id(&self) -> &RequestId {
|
||||
match self {
|
||||
$(Self::$variant { request_id, .. } => request_id,)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, JsonSchema)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ServerRequestPayload {
|
||||
@@ -846,7 +838,6 @@ server_notification_definitions! {
|
||||
CommandExecutionOutputDelta => "item/commandExecution/outputDelta" (v2::CommandExecutionOutputDeltaNotification),
|
||||
TerminalInteraction => "item/commandExecution/terminalInteraction" (v2::TerminalInteractionNotification),
|
||||
FileChangeOutputDelta => "item/fileChange/outputDelta" (v2::FileChangeOutputDeltaNotification),
|
||||
ServerRequestResolved => "serverRequest/resolved" (v2::ServerRequestResolvedNotification),
|
||||
McpToolCallProgress => "item/mcpToolCall/progress" (v2::McpToolCallProgressNotification),
|
||||
McpServerOauthLoginCompleted => "mcpServer/oauthLogin/completed" (v2::McpServerOauthLoginCompletedNotification),
|
||||
AccountUpdated => "account/updated" (v2::AccountUpdatedNotification),
|
||||
@@ -1115,7 +1106,6 @@ mod tests {
|
||||
);
|
||||
|
||||
let payload = ServerRequestPayload::ExecCommandApproval(params);
|
||||
assert_eq!(request.id(), &RequestId::Integer(7));
|
||||
assert_eq!(payload.request_with_id(RequestId::Integer(7)), request);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
pub mod common;
|
||||
mod mappers;
|
||||
mod serde_helpers;
|
||||
pub mod thread_history;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
|
||||
pub fn deserialize_double_option<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
|
||||
where
|
||||
T: Deserialize<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
serde_with::rust::double_option::deserialize(deserializer)
|
||||
}
|
||||
|
||||
pub fn serialize_double_option<T, S>(
|
||||
value: &Option<Option<T>>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
T: Serialize,
|
||||
S: Serializer,
|
||||
{
|
||||
serde_with::rust::double_option::serialize(value, serializer)
|
||||
}
|
||||
@@ -22,7 +22,6 @@ use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::ContextCompactedEvent;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
@@ -127,9 +126,6 @@ impl ThreadHistoryBuilder {
|
||||
EventMsg::WebSearchEnd(payload) => self.handle_web_search_end(payload),
|
||||
EventMsg::ExecCommandBegin(payload) => self.handle_exec_command_begin(payload),
|
||||
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
|
||||
EventMsg::ApplyPatchApprovalRequest(payload) => {
|
||||
self.handle_apply_patch_approval_request(payload)
|
||||
}
|
||||
EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload),
|
||||
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
|
||||
EventMsg::DynamicToolCallRequest(payload) => {
|
||||
@@ -368,19 +364,6 @@ impl ThreadHistoryBuilder {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
if payload.turn_id.is_empty() {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
@@ -1097,7 +1080,6 @@ mod tests {
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
@@ -1106,7 +1088,6 @@ mod tests {
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -1999,133 +1980,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_apply_begin_updates_active_turn_snapshot_with_file_change() {
|
||||
let turn_id = "turn-1";
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "apply patch".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "patch-call".into(),
|
||||
turn_id: turn_id.to_string(),
|
||||
auto_approved: false,
|
||||
changes: [(
|
||||
PathBuf::from("README.md"),
|
||||
codex_protocol::protocol::FileChange::Add {
|
||||
content: "hello\n".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
];
|
||||
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
|
||||
let snapshot = builder
|
||||
.active_turn_snapshot()
|
||||
.expect("active turn snapshot");
|
||||
assert_eq!(snapshot.id, turn_id);
|
||||
assert_eq!(snapshot.status, TurnStatus::InProgress);
|
||||
assert_eq!(
|
||||
snapshot.items,
|
||||
vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "apply patch".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::FileChange {
|
||||
id: "patch-call".into(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".into(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".into(),
|
||||
}],
|
||||
status: PatchApplyStatus::InProgress,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_approval_request_updates_active_turn_snapshot_with_file_change() {
|
||||
let turn_id = "turn-1";
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "apply patch".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "patch-call".into(),
|
||||
turn_id: turn_id.to_string(),
|
||||
changes: [(
|
||||
PathBuf::from("README.md"),
|
||||
codex_protocol::protocol::FileChange::Add {
|
||||
content: "hello\n".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
];
|
||||
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
|
||||
let snapshot = builder
|
||||
.active_turn_snapshot()
|
||||
.expect("active turn snapshot");
|
||||
assert_eq!(snapshot.id, turn_id);
|
||||
assert_eq!(snapshot.status, TurnStatus::InProgress);
|
||||
assert_eq!(
|
||||
snapshot.items,
|
||||
vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "apply patch".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::FileChange {
|
||||
id: "patch-call".into(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".into(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".into(),
|
||||
}],
|
||||
status: PatchApplyStatus::InProgress,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_turn_complete_does_not_close_active_turn() {
|
||||
let events = vec![
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ForcedLoginMethod;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -420,13 +419,6 @@ pub struct SendUserTurnParams {
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox_policy: SandboxPolicy,
|
||||
pub model: String,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
pub effort: Option<ReasoningEffort>,
|
||||
pub summary: ReasoningSummary,
|
||||
/// Optional JSON Schema used to constrain the final assistant message for this turn.
|
||||
@@ -437,55 +429,6 @@ pub struct SendUserTurnParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendUserTurnResponse {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn send_user_turn_params_preserve_explicit_null_service_tier() {
|
||||
let params = SendUserTurnParams {
|
||||
conversation_id: ThreadId::new(),
|
||||
items: vec![],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: "gpt-4.1".to_string(),
|
||||
service_tier: Some(None),
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
output_schema: None,
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("serviceTier"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let roundtrip: SendUserTurnParams =
|
||||
serde_json::from_value(serialized).expect("params should deserialize");
|
||||
assert_eq!(roundtrip.service_tier, Some(None));
|
||||
|
||||
let without_override = SendUserTurnParams {
|
||||
conversation_id: ThreadId::new(),
|
||||
items: vec![],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: "gpt-4.1".to_string(),
|
||||
service_tier: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
output_schema: None,
|
||||
};
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(&without_override).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InterruptConversationParams {
|
||||
@@ -612,7 +555,6 @@ pub struct LoginChatGptCompleteNotification {
|
||||
pub struct SessionConfiguredNotification {
|
||||
pub session_id: ThreadId,
|
||||
pub model: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub history_log_id: u64,
|
||||
#[ts(type = "number")]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::RequestId;
|
||||
use crate::protocol::common::AuthMode;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::account::PlanType;
|
||||
@@ -17,7 +16,6 @@ use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::config_types::Verbosity;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
@@ -393,7 +391,6 @@ pub struct ProfileV2 {
|
||||
pub model: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
pub approval_policy: Option<AskForApproval>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
@@ -505,7 +502,6 @@ pub struct Config {
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub model_verbosity: Option<Verbosity>,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub analytics: Option<AnalyticsConfig>,
|
||||
#[experimental("config/read.apps")]
|
||||
#[serde(default)]
|
||||
@@ -1791,14 +1787,6 @@ pub struct ThreadStartParams {
|
||||
pub model: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -1861,7 +1849,6 @@ pub struct ThreadStartResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
@@ -1903,14 +1890,6 @@ pub struct ThreadResumeParams {
|
||||
pub model: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -1939,7 +1918,6 @@ pub struct ThreadResumeResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
@@ -1972,14 +1950,6 @@ pub struct ThreadForkParams {
|
||||
pub model: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub model_provider: Option<String>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub cwd: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
@@ -2006,7 +1976,6 @@ pub struct ThreadForkResponse {
|
||||
pub thread: Thread,
|
||||
pub model: String,
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
pub approval_policy: AskForApproval,
|
||||
pub sandbox: SandboxPolicy,
|
||||
@@ -2547,8 +2516,6 @@ pub struct Thread {
|
||||
pub id: String,
|
||||
/// Usually the first user message in the thread, if available.
|
||||
pub preview: String,
|
||||
/// Whether the thread is ephemeral and should not be materialized on disk.
|
||||
pub ephemeral: bool,
|
||||
/// Model provider used for this thread (for example, 'openai').
|
||||
pub model_provider: String,
|
||||
/// Unix timestamp (in seconds) when the thread was created.
|
||||
@@ -2587,7 +2554,6 @@ pub struct Thread {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AccountUpdatedNotification {
|
||||
pub auth_mode: Option<AuthMode>,
|
||||
pub plan_type: Option<PlanType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -2867,15 +2833,6 @@ pub struct TurnStartParams {
|
||||
/// Override the model for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub model: Option<String>,
|
||||
/// Override the service tier for this turn and subsequent turns.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "super::serde_helpers::deserialize_double_option",
|
||||
serialize_with = "super::serde_helpers::serialize_double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
#[ts(optional = nullable)]
|
||||
pub service_tier: Option<Option<ServiceTier>>,
|
||||
/// Override the reasoning effort for this turn and subsequent turns.
|
||||
#[ts(optional = nullable)]
|
||||
pub effort: Option<ReasoningEffort>,
|
||||
@@ -3788,14 +3745,6 @@ pub struct FileChangeOutputDeltaNotification {
|
||||
pub delta: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ServerRequestResolvedNotification {
|
||||
pub thread_id: String,
|
||||
pub request_id: RequestId,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -4605,56 +4554,4 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: ThreadStartParams = serde_json::from_value(json!({ "serviceTier": null }))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.service_tier, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("serviceTier"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(ThreadStartParams::default()).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
"threadId": "thread_123",
|
||||
"input": [],
|
||||
"serviceTier": null
|
||||
}))
|
||||
.expect("params should deserialize");
|
||||
assert_eq!(params.service_tier, Some(None));
|
||||
|
||||
let serialized = serde_json::to_value(¶ms).expect("params should serialize");
|
||||
assert_eq!(
|
||||
serialized.get("serviceTier"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
let without_override = TurnStartParams {
|
||||
thread_id: "thread_123".to_string(),
|
||||
input: vec![],
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
model: None,
|
||||
service_tier: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
};
|
||||
let serialized_without_override =
|
||||
serde_json::to_value(&without_override).expect("params should serialize");
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,6 @@ cargo run -p codex-app-server-test-client -- \
|
||||
cargo run -p codex-app-server-test-client -- model-list
|
||||
```
|
||||
|
||||
## Watching Raw Inbound Traffic
|
||||
|
||||
Initialize a connection, then print every inbound JSON-RPC message until you stop it with
|
||||
`Ctrl+C`:
|
||||
|
||||
```bash
|
||||
cargo run -p codex-app-server-test-client -- watch
|
||||
```
|
||||
|
||||
## Testing Thread Rejoin Behavior
|
||||
|
||||
Build and start an app server using commands above. The app-server log is written to `/tmp/codex-app-server-test-client/app-server.log`
|
||||
|
||||
@@ -15,7 +15,6 @@ use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
@@ -72,7 +71,6 @@ use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
@@ -106,8 +104,6 @@ const NOTIFICATIONS_TO_OPT_OUT: &[&str] = &[
|
||||
"item/reasoning/summaryTextDelta",
|
||||
"item/reasoning/textDelta",
|
||||
];
|
||||
const APP_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const APP_SERVER_GRACEFUL_SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Minimal launcher that initializes the Codex app-server and logs the handshake.
|
||||
#[derive(Parser)]
|
||||
@@ -192,10 +188,6 @@ enum CliCommand {
|
||||
/// Existing thread id to resume.
|
||||
thread_id: String,
|
||||
},
|
||||
/// Initialize the app-server and dump all inbound messages until interrupted.
|
||||
///
|
||||
/// This command does not auto-exit; stop it with SIGINT/SIGTERM/SIGKILL.
|
||||
Watch,
|
||||
/// Start a V2 turn that elicits an ExecCommand approval.
|
||||
#[command(name = "trigger-cmd-approval")]
|
||||
TriggerCmdApproval {
|
||||
@@ -299,11 +291,6 @@ pub fn run() -> Result<()> {
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
thread_resume_follow(&endpoint, &config_overrides, thread_id)
|
||||
}
|
||||
CliCommand::Watch => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "watch")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
watch(&endpoint, &config_overrides)
|
||||
}
|
||||
CliCommand::TriggerCmdApproval { user_message } => {
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
trigger_cmd_approval(&endpoint, &config_overrides, user_message, &dynamic_tools)
|
||||
@@ -502,26 +489,25 @@ fn send_message(
|
||||
config_overrides: &[String],
|
||||
user_message: String,
|
||||
) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let conversation = client.start_thread()?;
|
||||
println!("< newConversation response: {conversation:?}");
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let subscription = client.add_conversation_listener(&conversation.conversation_id)?;
|
||||
println!("< addConversationListener response: {subscription:?}");
|
||||
let conversation = client.start_thread()?;
|
||||
println!("< newConversation response: {conversation:?}");
|
||||
|
||||
let send_response =
|
||||
client.send_user_message(&conversation.conversation_id, &user_message)?;
|
||||
println!("< sendUserMessage response: {send_response:?}");
|
||||
let subscription = client.add_conversation_listener(&conversation.conversation_id)?;
|
||||
println!("< addConversationListener response: {subscription:?}");
|
||||
|
||||
client.stream_conversation(&conversation.conversation_id)?;
|
||||
let send_response = client.send_user_message(&conversation.conversation_id, &user_message)?;
|
||||
println!("< sendUserMessage response: {send_response:?}");
|
||||
|
||||
client.remove_thread_listener(subscription.subscription_id)?;
|
||||
client.stream_conversation(&conversation.conversation_id)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
client.remove_thread_listener(subscription.subscription_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_message_v2(
|
||||
@@ -579,85 +565,82 @@ fn trigger_zsh_fork_multi_cmd_approval(
|
||||
let default_prompt = "Run this exact command using shell command execution without rewriting or splitting it: /usr/bin/true && /usr/bin/true";
|
||||
let message = user_message.unwrap_or_else(|| default_prompt.to_string());
|
||||
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
|
||||
client.command_approval_behavior = match abort_on {
|
||||
Some(index) => CommandApprovalBehavior::AbortOn(index),
|
||||
None => CommandApprovalBehavior::AlwaysAccept,
|
||||
};
|
||||
client.command_approval_count = 0;
|
||||
client.command_approval_item_ids.clear();
|
||||
client.command_execution_statuses.clear();
|
||||
client.last_turn_status = None;
|
||||
client.command_approval_behavior = match abort_on {
|
||||
Some(index) => CommandApprovalBehavior::AbortOn(index),
|
||||
None => CommandApprovalBehavior::AlwaysAccept,
|
||||
};
|
||||
client.command_approval_count = 0;
|
||||
client.command_approval_item_ids.clear();
|
||||
client.command_execution_statuses.clear();
|
||||
client.last_turn_status = None;
|
||||
|
||||
let mut turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: message,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
turn_params.approval_policy = Some(AskForApproval::OnRequest);
|
||||
turn_params.sandbox_policy = Some(SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
});
|
||||
let mut turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: message,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
turn_params.approval_policy = Some(AskForApproval::OnRequest);
|
||||
turn_params.sandbox_policy = Some(SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
});
|
||||
|
||||
let turn_response = client.turn_start(turn_params)?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?;
|
||||
let turn_response = client.turn_start(turn_params)?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?;
|
||||
|
||||
if client.command_approval_count < min_approvals {
|
||||
bail!(
|
||||
"expected at least {min_approvals} command approvals, got {}",
|
||||
client.command_approval_count
|
||||
);
|
||||
}
|
||||
let mut approvals_per_item = std::collections::BTreeMap::new();
|
||||
for item_id in &client.command_approval_item_ids {
|
||||
*approvals_per_item.entry(item_id.clone()).or_insert(0usize) += 1;
|
||||
}
|
||||
let max_approvals_for_one_item = approvals_per_item.values().copied().max().unwrap_or(0);
|
||||
if max_approvals_for_one_item < min_approvals {
|
||||
bail!(
|
||||
"expected at least {min_approvals} approvals for one command item, got max {max_approvals_for_one_item} with map {approvals_per_item:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let last_command_status = client.command_execution_statuses.last();
|
||||
if abort_on.is_none() {
|
||||
if last_command_status != Some(&CommandExecutionStatus::Completed) {
|
||||
bail!("expected completed command execution, got {last_command_status:?}");
|
||||
}
|
||||
if client.last_turn_status != Some(TurnStatus::Completed) {
|
||||
bail!(
|
||||
"expected completed turn in all-accept flow, got {:?}",
|
||||
client.last_turn_status
|
||||
);
|
||||
}
|
||||
} else if last_command_status == Some(&CommandExecutionStatus::Completed) {
|
||||
bail!(
|
||||
"expected non-completed command execution in mixed approval/decline flow, got {last_command_status:?}"
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"[zsh-fork multi-approval summary] approvals={}, approvals_per_item={approvals_per_item:?}, command_statuses={:?}, turn_status={:?}",
|
||||
client.command_approval_count,
|
||||
client.command_execution_statuses,
|
||||
client.last_turn_status
|
||||
if client.command_approval_count < min_approvals {
|
||||
bail!(
|
||||
"expected at least {min_approvals} command approvals, got {}",
|
||||
client.command_approval_count
|
||||
);
|
||||
}
|
||||
let mut approvals_per_item = std::collections::BTreeMap::new();
|
||||
for item_id in &client.command_approval_item_ids {
|
||||
*approvals_per_item.entry(item_id.clone()).or_insert(0usize) += 1;
|
||||
}
|
||||
let max_approvals_for_one_item = approvals_per_item.values().copied().max().unwrap_or(0);
|
||||
if max_approvals_for_one_item < min_approvals {
|
||||
bail!(
|
||||
"expected at least {min_approvals} approvals for one command item, got max {max_approvals_for_one_item} with map {approvals_per_item:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let last_command_status = client.command_execution_statuses.last();
|
||||
if abort_on.is_none() {
|
||||
if last_command_status != Some(&CommandExecutionStatus::Completed) {
|
||||
bail!("expected completed command execution, got {last_command_status:?}");
|
||||
}
|
||||
if client.last_turn_status != Some(TurnStatus::Completed) {
|
||||
bail!(
|
||||
"expected completed turn in all-accept flow, got {:?}",
|
||||
client.last_turn_status
|
||||
);
|
||||
}
|
||||
} else if last_command_status == Some(&CommandExecutionStatus::Completed) {
|
||||
bail!(
|
||||
"expected non-completed command execution in mixed approval/decline flow, got {last_command_status:?}"
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"[zsh-fork multi-approval summary] approvals={}, approvals_per_item={approvals_per_item:?}, command_statuses={:?}, turn_status={:?}",
|
||||
client.command_approval_count, client.command_execution_statuses, client.last_turn_status
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resume_message_v2(
|
||||
@@ -669,30 +652,30 @@ fn resume_message_v2(
|
||||
) -> Result<()> {
|
||||
ensure_dynamic_tools_unused(dynamic_tools, "resume-message-v2")?;
|
||||
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let resume_response = client.thread_resume(ThreadResumeParams {
|
||||
thread_id,
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/resume response: {resume_response:?}");
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let turn_response = client.turn_start(TurnStartParams {
|
||||
thread_id: resume_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
let resume_response = client.thread_resume(ThreadResumeParams {
|
||||
thread_id,
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/resume response: {resume_response:?}");
|
||||
|
||||
client.stream_turn(&resume_response.thread.id, &turn_response.turn.id)?;
|
||||
let turn_response = client.turn_start(TurnStartParams {
|
||||
thread_id: resume_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
client.stream_turn(&resume_response.thread.id, &turn_response.turn.id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thread_resume_follow(
|
||||
@@ -715,16 +698,6 @@ fn thread_resume_follow(
|
||||
client.stream_notifications_forever()
|
||||
}
|
||||
|
||||
fn watch(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()> {
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
println!("< streaming inbound messages until process is terminated");
|
||||
|
||||
client.stream_notifications_forever()
|
||||
}
|
||||
|
||||
fn trigger_cmd_approval(
|
||||
endpoint: &Endpoint,
|
||||
config_overrides: &[String],
|
||||
@@ -795,34 +768,34 @@ fn send_message_v2_with_policies(
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize_with_experimental_api(experimental_api)?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
let mut turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
turn_params.approval_policy = approval_policy;
|
||||
turn_params.sandbox_policy = sandbox_policy;
|
||||
let initialize = client.initialize_with_experimental_api(experimental_api)?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let turn_response = client.turn_start(turn_params)?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
let mut turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
turn_params.approval_policy = approval_policy;
|
||||
turn_params.sandbox_policy = sandbox_policy;
|
||||
|
||||
client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?;
|
||||
let turn_response = client.turn_start(turn_params)?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_follow_up_v2(
|
||||
@@ -832,130 +805,119 @@ fn send_follow_up_v2(
|
||||
follow_up_message: String,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let first_turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: first_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let first_turn_response = client.turn_start(first_turn_params)?;
|
||||
println!("< turn/start response (initial): {first_turn_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &first_turn_response.turn.id)?;
|
||||
let thread_response = client.thread_start(ThreadStartParams {
|
||||
dynamic_tools: dynamic_tools.clone(),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
|
||||
let follow_up_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: follow_up_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let follow_up_response = client.turn_start(follow_up_params)?;
|
||||
println!("< turn/start response (follow-up): {follow_up_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &follow_up_response.turn.id)?;
|
||||
let first_turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: first_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let first_turn_response = client.turn_start(first_turn_params)?;
|
||||
println!("< turn/start response (initial): {first_turn_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &first_turn_response.turn.id)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let follow_up_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: follow_up_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let follow_up_response = client.turn_start(follow_up_params)?;
|
||||
println!("< turn/start response (follow-up): {follow_up_response:?}");
|
||||
client.stream_turn(&thread_response.thread.id, &follow_up_response.turn.id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_login(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let login_response = client.login_chat_gpt()?;
|
||||
println!("< loginChatGpt response: {login_response:?}");
|
||||
println!(
|
||||
"Open the following URL in your browser to continue:\n{}",
|
||||
login_response.auth_url
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let login_response = client.login_chat_gpt()?;
|
||||
println!("< loginChatGpt response: {login_response:?}");
|
||||
println!(
|
||||
"Open the following URL in your browser to continue:\n{}",
|
||||
login_response.auth_url
|
||||
);
|
||||
|
||||
let completion = client.wait_for_login_completion(&login_response.login_id)?;
|
||||
println!("< loginChatGptComplete notification: {completion:?}");
|
||||
|
||||
if completion.success {
|
||||
println!("Login succeeded.");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(
|
||||
"login failed: {}",
|
||||
completion
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("unknown error from loginChatGptComplete")
|
||||
);
|
||||
|
||||
let completion = client.wait_for_login_completion(&login_response.login_id)?;
|
||||
println!("< loginChatGptComplete notification: {completion:?}");
|
||||
|
||||
if completion.success {
|
||||
println!("Login succeeded.");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(
|
||||
"login failed: {}",
|
||||
completion
|
||||
.error
|
||||
.as_deref()
|
||||
.unwrap_or("unknown error from loginChatGptComplete")
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_account_rate_limits(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let response = client.get_account_rate_limits()?;
|
||||
println!("< account/rateLimits/read response: {response:?}");
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let response = client.get_account_rate_limits()?;
|
||||
println!("< account/rateLimits/read response: {response:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn model_list(endpoint: &Endpoint, config_overrides: &[String]) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
let response = client.model_list(ModelListParams::default())?;
|
||||
println!("< model/list response: {response:?}");
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
let response = client.model_list(ModelListParams::default())?;
|
||||
println!("< model/list response: {response:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u32) -> Result<()> {
|
||||
with_client(endpoint, config_overrides, |client| {
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let response = client.thread_list(ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(limit),
|
||||
sort_key: None,
|
||||
model_providers: None,
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
search_term: None,
|
||||
})?;
|
||||
println!("< thread/list response: {response:?}");
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn with_client<T>(
|
||||
endpoint: &Endpoint,
|
||||
config_overrides: &[String],
|
||||
f: impl FnOnce(&mut CodexClient) -> Result<T>,
|
||||
) -> Result<T> {
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
let result = f(&mut client);
|
||||
client.print_trace_summary();
|
||||
result
|
||||
|
||||
let initialize = client.initialize()?;
|
||||
println!("< initialize response: {initialize:?}");
|
||||
|
||||
let response = client.thread_list(ThreadListParams {
|
||||
cursor: None,
|
||||
limit: Some(limit),
|
||||
sort_key: None,
|
||||
model_providers: None,
|
||||
source_kinds: None,
|
||||
archived: None,
|
||||
cwd: None,
|
||||
search_term: None,
|
||||
})?;
|
||||
println!("< thread/list response: {response:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_dynamic_tools_unused(
|
||||
@@ -1012,8 +974,6 @@ struct CodexClient {
|
||||
command_approval_item_ids: Vec<String>,
|
||||
command_execution_statuses: Vec<CommandExecutionStatus>,
|
||||
last_turn_status: Option<TurnStatus>,
|
||||
trace_id: String,
|
||||
trace_root_span_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -1073,8 +1033,6 @@ impl CodexClient {
|
||||
command_approval_item_ids: Vec::new(),
|
||||
command_execution_statuses: Vec::new(),
|
||||
last_turn_status: None,
|
||||
trace_id: generate_trace_id(),
|
||||
trace_root_span_id: generate_parent_span_id(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1096,8 +1054,6 @@ impl CodexClient {
|
||||
command_approval_item_ids: Vec::new(),
|
||||
command_execution_statuses: Vec::new(),
|
||||
last_turn_status: None,
|
||||
trace_id: generate_trace_id(),
|
||||
trace_root_span_id: generate_parent_span_id(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1463,32 +1419,12 @@ impl CodexClient {
|
||||
}
|
||||
|
||||
fn write_request(&mut self, request: &ClientRequest) -> Result<()> {
|
||||
let request = self.jsonrpc_request_with_trace(request)?;
|
||||
let request_json = serde_json::to_string(&request)?;
|
||||
let request_pretty = serde_json::to_string_pretty(&request)?;
|
||||
let request_json = serde_json::to_string(request)?;
|
||||
let request_pretty = serde_json::to_string_pretty(request)?;
|
||||
print_multiline_with_prefix("> ", &request_pretty);
|
||||
self.write_payload(&request_json)
|
||||
}
|
||||
|
||||
fn jsonrpc_request_with_trace(&self, request: &ClientRequest) -> Result<JSONRPCRequest> {
|
||||
let request_value = serde_json::to_value(request)?;
|
||||
let mut request: JSONRPCRequest = serde_json::from_value(request_value)
|
||||
.context("client request was not a valid JSON-RPC request")?;
|
||||
request.trace = Some(W3cTraceContext {
|
||||
traceparent: Some(format!(
|
||||
"00-{}-{}-01",
|
||||
self.trace_id, self.trace_root_span_id
|
||||
)),
|
||||
tracestate: None,
|
||||
});
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
fn print_trace_summary(&self) {
|
||||
println!("\n[Datadog trace]");
|
||||
println!("go/trace/{}\n", self.trace_id);
|
||||
}
|
||||
|
||||
fn wait_for_response<T>(&mut self, request_id: RequestId, method: &str) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
@@ -1754,15 +1690,6 @@ impl CodexClient {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_trace_id() -> String {
|
||||
Uuid::new_v4().simple().to_string()
|
||||
}
|
||||
|
||||
fn generate_parent_span_id() -> String {
|
||||
let uuid = Uuid::new_v4().simple().to_string();
|
||||
uuid[..16].to_string()
|
||||
}
|
||||
|
||||
fn print_multiline_with_prefix(prefix: &str, payload: &str) {
|
||||
for line in payload.lines() {
|
||||
println!("{prefix}{line}");
|
||||
@@ -1782,18 +1709,11 @@ impl Drop for CodexClient {
|
||||
return;
|
||||
}
|
||||
|
||||
let deadline = SystemTime::now() + APP_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT;
|
||||
loop {
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
println!("[codex app-server exited: {status}]");
|
||||
return;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
if SystemTime::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
|
||||
thread::sleep(APP_SERVER_GRACEFUL_SHUTDOWN_POLL_INTERVAL);
|
||||
if let Ok(Some(status)) = child.try_wait() {
|
||||
println!("[codex app-server exited: {status}]");
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = child.kill();
|
||||
|
||||
@@ -21,7 +21,6 @@ async-trait = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
|
||||
@@ -62,8 +62,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat
|
||||
|
||||
- Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected.
|
||||
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history.
|
||||
The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`.
|
||||
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running.
|
||||
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers a `turn/started` notification.
|
||||
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
|
||||
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
|
||||
|
||||
@@ -120,16 +119,16 @@ Example with notification opt-out:
|
||||
|
||||
## API Overview
|
||||
|
||||
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread.
|
||||
- `thread/start` — create a new thread; emits `thread/started` and auto-subscribes you to turn/item events for that thread.
|
||||
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for the new thread.
|
||||
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; emits `thread/started` and auto-subscribes you to turn/item events for the new thread.
|
||||
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/loaded/list` — list the thread ids currently loaded in memory.
|
||||
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
|
||||
- `thread/status/changed` — notification emitted when a loaded thread’s status changes (`threadId` + new `status`).
|
||||
- `thread/archive` — move a thread’s rollout file into the archived directory; returns `{}` on success and emits `thread/archived`.
|
||||
- `thread/unsubscribe` — unsubscribe this connection from thread turn/item events. If this was the last subscriber, the server shuts down and unloads the thread, then emits `thread/closed`.
|
||||
- `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
|
||||
- `thread/name/set` — set or update a thread’s user-facing name; returns `{}` on success. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
|
||||
@@ -273,11 +272,10 @@ When `nextCursor` is `null`, you’ve reached the final page.
|
||||
|
||||
### Example: Track thread status changes
|
||||
|
||||
`thread/status/changed` is emitted whenever a loaded thread's status changes after it has already been introduced to the client:
|
||||
`thread/status/changed` is emitted whenever a loaded thread's status changes:
|
||||
|
||||
- Includes `threadId` and the new `status`.
|
||||
- Status can be `notLoaded`, `idle`, `systemError`, or `active` (with `activeFlags`; `active` implies running).
|
||||
- `thread/start`, `thread/fork`, and detached review threads do not emit a separate initial `thread/status/changed`; their `thread/started` notification already carries the current `thread.status`.
|
||||
|
||||
```json
|
||||
{ "method": "thread/status/changed", "params": {
|
||||
@@ -621,7 +619,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o
|
||||
|
||||
### Turn events
|
||||
|
||||
The app-server streams JSON-RPC notifications while a turn is running. Each turn emits `turn/started` when it begins running and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
|
||||
The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with `turn/started` (initial `turn`) and ends with `turn/completed` (final `turn` status). Token usage events stream separately via `thread/tokenUsage/updated`. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: `item/started` → zero or more item-specific deltas → `item/completed`.
|
||||
|
||||
- `turn/started` — `{ turn }` with the turn id, empty `items`, and `status: "inProgress"`.
|
||||
- `turn/completed` — `{ turn }` where `turn.status` is `completed`, `interrupted`, or `failed`; failures carry `{ error: { message, codexErrorInfo?, additionalDetails? } }`.
|
||||
@@ -714,8 +712,7 @@ Order of messages:
|
||||
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
|
||||
3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`.
|
||||
4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt.
|
||||
5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
|
||||
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
|
||||
|
||||
### File change approvals
|
||||
|
||||
@@ -724,15 +721,10 @@ Order of messages:
|
||||
1. `item/started` — emits a `fileChange` item with `changes` (diff chunk summaries) and `status: "inProgress"`. Show the proposed edits and paths to the user.
|
||||
2. `item/fileChange/requestApproval` (request) — includes `itemId`, `threadId`, `turnId`, and an optional `reason`.
|
||||
3. Client response — `{ "decision": "accept" }` or `{ "decision": "decline" }`.
|
||||
4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt.
|
||||
5. `item/completed` — returns the same `fileChange` item with `status` updated to `completed`, `failed`, or `declined` after the patch attempt. Rely on this to show success/failure and finalize the diff state in your UI.
|
||||
4. `item/completed` — returns the same `fileChange` item with `status` updated to `completed`, `failed`, or `declined` after the patch attempt. Rely on this to show success/failure and finalize the diff state in your UI.
|
||||
|
||||
UI guidance for IDEs: surface an approval dialog as soon as the request arrives. The turn will proceed after the server receives a response to the approval request. The terminal `item/completed` notification will be sent with the appropriate status.
|
||||
|
||||
### request_user_input
|
||||
|
||||
When the client responds to `item/tool/requestUserInput`, the server emits `serverRequest/resolved` with `{ threadId, requestId }`. If the pending request is cleared by turn start, turn completion, or turn interruption before the client answers, the server emits the same notification for that cleanup.
|
||||
|
||||
### Dynamic tool calls (experimental)
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
@@ -953,7 +945,7 @@ The JSON-RPC auth/account surface exposes request/response methods plus server-i
|
||||
|
||||
### Authentication modes
|
||||
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`), which also includes the current ChatGPT `planType` when available, and can be inferred from `account/read`.
|
||||
Codex supports these authentication modes. The current mode is surfaced in `account/updated` (`authMode`) and can be inferred from `account/read`.
|
||||
|
||||
- **API key (`apiKey`)**: Caller supplies an OpenAI API key via `account/login/start` with `type: "apiKey"`. The API key is saved and used for API requests.
|
||||
- **ChatGPT managed (`chatgpt`)** (recommended): Codex owns the ChatGPT OAuth flow and refresh tokens. Start via `account/login/start` with `type: "chatgpt"`; Codex persists tokens to disk and refreshes them automatically.
|
||||
@@ -965,7 +957,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
|
||||
- `account/login/completed` (notify) — emitted when a login attempt finishes (success or error).
|
||||
- `account/login/cancel` — cancel a pending ChatGPT login by `loginId`.
|
||||
- `account/logout` — sign out; triggers `account/updated`.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available.
|
||||
- `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`).
|
||||
- `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify).
|
||||
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change.
|
||||
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
|
||||
@@ -1009,7 +1001,7 @@ Field notes:
|
||||
3. Notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey", "planType": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "apikey" } }
|
||||
```
|
||||
|
||||
### 3) Log in with ChatGPT (browser flow)
|
||||
@@ -1023,7 +1015,7 @@ Field notes:
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt", "planType": "plus" } }
|
||||
{ "method": "account/updated", "params": { "authMode": "chatgpt" } }
|
||||
```
|
||||
|
||||
### 4) Cancel a ChatGPT login
|
||||
@@ -1038,7 +1030,7 @@ Field notes:
|
||||
```json
|
||||
{ "method": "account/logout", "id": 5 }
|
||||
{ "id": 5, "result": {} }
|
||||
{ "method": "account/updated", "params": { "authMode": null, "planType": null } }
|
||||
{ "method": "account/updated", "params": { "authMode": null } }
|
||||
```
|
||||
|
||||
### 6) Rate limits (ChatGPT)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
use crate::message_processor::ConnectionSessionState;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::transport::AppServerTransport;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_otel::set_parent_from_context;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_otel::traceparent_context_from_env;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use tracing::Span;
|
||||
use tracing::field;
|
||||
use tracing::info_span;
|
||||
|
||||
pub(crate) fn request_span(
|
||||
request: &JSONRPCRequest,
|
||||
transport: AppServerTransport,
|
||||
connection_id: ConnectionId,
|
||||
session: &ConnectionSessionState,
|
||||
) -> Span {
|
||||
let span = info_span!(
|
||||
"app_server.request",
|
||||
otel.kind = "server",
|
||||
otel.name = request.method.as_str(),
|
||||
rpc.system = "jsonrpc",
|
||||
rpc.method = request.method.as_str(),
|
||||
rpc.transport = transport_name(transport),
|
||||
rpc.request_id = ?request.id,
|
||||
app_server.connection_id = ?connection_id,
|
||||
app_server.api_version = "v2",
|
||||
app_server.client_name = field::Empty,
|
||||
app_server.client_version = field::Empty,
|
||||
);
|
||||
|
||||
let initialize_client_info = initialize_client_info(request);
|
||||
if let Some(client_name) = client_name(initialize_client_info.as_ref(), session) {
|
||||
span.record("app_server.client_name", client_name);
|
||||
}
|
||||
if let Some(client_version) = client_version(initialize_client_info.as_ref(), session) {
|
||||
span.record("app_server.client_version", client_version);
|
||||
}
|
||||
|
||||
if let Some(traceparent) = request
|
||||
.trace
|
||||
.as_ref()
|
||||
.and_then(|trace| trace.traceparent.as_deref())
|
||||
{
|
||||
let trace = W3cTraceContext {
|
||||
traceparent: Some(traceparent.to_string()),
|
||||
tracestate: request
|
||||
.trace
|
||||
.as_ref()
|
||||
.and_then(|value| value.tracestate.clone()),
|
||||
};
|
||||
if !set_parent_from_w3c_trace_context(&span, &trace) {
|
||||
tracing::warn!(
|
||||
rpc_method = request.method.as_str(),
|
||||
rpc_request_id = ?request.id,
|
||||
"ignoring invalid inbound request trace carrier"
|
||||
);
|
||||
}
|
||||
} else if let Some(context) = traceparent_context_from_env() {
|
||||
set_parent_from_context(&span, context);
|
||||
}
|
||||
|
||||
span
|
||||
}
|
||||
|
||||
fn transport_name(transport: AppServerTransport) -> &'static str {
|
||||
match transport {
|
||||
AppServerTransport::Stdio => "stdio",
|
||||
AppServerTransport::WebSocket { .. } => "websocket",
|
||||
}
|
||||
}
|
||||
|
||||
fn client_name<'a>(
|
||||
initialize_client_info: Option<&'a InitializeParams>,
|
||||
session: &'a ConnectionSessionState,
|
||||
) -> Option<&'a str> {
|
||||
if let Some(params) = initialize_client_info {
|
||||
return Some(params.client_info.name.as_str());
|
||||
}
|
||||
session.app_server_client_name.as_deref()
|
||||
}
|
||||
|
||||
fn client_version<'a>(
|
||||
initialize_client_info: Option<&'a InitializeParams>,
|
||||
session: &'a ConnectionSessionState,
|
||||
) -> Option<&'a str> {
|
||||
if let Some(params) = initialize_client_info {
|
||||
return Some(params.client_info.version.as_str());
|
||||
}
|
||||
session.client_version.as_deref()
|
||||
}
|
||||
|
||||
fn initialize_client_info(request: &JSONRPCRequest) -> Option<InitializeParams> {
|
||||
if request.method != "initialize" {
|
||||
return None;
|
||||
}
|
||||
let params = request.params.clone()?;
|
||||
serde_json::from_value(params).ok()
|
||||
}
|
||||
@@ -6,8 +6,6 @@ use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::outgoing_message::ClientRequestResult;
|
||||
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
||||
use crate::server_request_error::is_turn_transition_server_request_error;
|
||||
use crate::thread_state::ThreadListenerCommand;
|
||||
use crate::thread_state::ThreadState;
|
||||
use crate::thread_state::TurnSummary;
|
||||
use crate::thread_status::ThreadWatchActiveGuard;
|
||||
@@ -58,7 +56,6 @@ use codex_app_server_protocol::RawResponseItemCompletedNotification;
|
||||
use codex_app_server_protocol::ReasoningSummaryPartAddedNotification;
|
||||
use codex_app_server_protocol::ReasoningSummaryTextDeltaNotification;
|
||||
use codex_app_server_protocol::ReasoningTextDeltaNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::TerminalInteractionNotification;
|
||||
@@ -83,7 +80,6 @@ use codex_app_server_protocol::TurnError;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnPlanStep;
|
||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use codex_app_server_protocol::convert_patch_changes;
|
||||
@@ -136,38 +132,6 @@ struct CommandExecutionCompletionItem {
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
}
|
||||
|
||||
async fn resolve_server_request_on_thread_listener(
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
request_id: RequestId,
|
||||
) {
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
let listener_command_tx = {
|
||||
let state = thread_state.lock().await;
|
||||
state.listener_command_tx()
|
||||
};
|
||||
let Some(listener_command_tx) = listener_command_tx else {
|
||||
error!("failed to remove pending client request: thread listener is not running");
|
||||
return;
|
||||
};
|
||||
|
||||
if listener_command_tx
|
||||
.send(ThreadListenerCommand::ResolveServerRequest {
|
||||
request_id,
|
||||
completion_tx,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
error!(
|
||||
"failed to remove pending client request: thread listener command channel is closed"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = completion_rx.await {
|
||||
error!("failed to remove pending client request: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn apply_bespoke_event_handling(
|
||||
event: Event,
|
||||
@@ -186,34 +150,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
msg,
|
||||
} = event;
|
||||
match msg {
|
||||
EventMsg::TurnStarted(payload) => {
|
||||
// While not technically necessary as it was already done on TurnComplete, be extra cautios and abort any pending server requests.
|
||||
outgoing.abort_pending_server_requests().await;
|
||||
EventMsg::TurnStarted(_) => {
|
||||
thread_watch_manager
|
||||
.note_turn_started(&conversation_id.to_string())
|
||||
.await;
|
||||
if let ApiVersion::V2 = api_version {
|
||||
let turn = {
|
||||
let state = thread_state.lock().await;
|
||||
state.active_turn_snapshot().unwrap_or_else(|| Turn {
|
||||
id: payload.turn_id.clone(),
|
||||
items: Vec::new(),
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
})
|
||||
};
|
||||
let notification = TurnStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnStarted(notification))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
EventMsg::TurnComplete(_ev) => {
|
||||
// All per-thread requests are bound to a turn, so abort them.
|
||||
outgoing.abort_pending_server_requests().await;
|
||||
let turn_failed = thread_state.lock().await.turn_summary.last_error.is_some();
|
||||
thread_watch_manager
|
||||
.note_turn_completed(&conversation_id.to_string(), turn_failed)
|
||||
@@ -251,6 +193,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
EventMsg::RealtimeConversationRealtime(event) => {
|
||||
if let ApiVersion::V2 = api_version {
|
||||
match event.payload {
|
||||
RealtimeEvent::SessionCreated { .. } => {}
|
||||
RealtimeEvent::SessionUpdated { .. } => {}
|
||||
RealtimeEvent::AudioOut(audio) => {
|
||||
let notification = ThreadRealtimeOutputAudioDeltaNotification {
|
||||
@@ -274,24 +217,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
))
|
||||
.await;
|
||||
}
|
||||
RealtimeEvent::ConversationItemDone { .. } => {}
|
||||
RealtimeEvent::HandoffRequested(handoff) => {
|
||||
let notification = ThreadRealtimeItemAddedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
item: serde_json::json!({
|
||||
"type": "handoff_request",
|
||||
"handoff_id": handoff.handoff_id,
|
||||
"item_id": handoff.item_id,
|
||||
"input_transcript": handoff.input_transcript,
|
||||
"messages": handoff.messages,
|
||||
}),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ThreadRealtimeItemAdded(
|
||||
notification,
|
||||
))
|
||||
.await;
|
||||
}
|
||||
RealtimeEvent::Error(message) => {
|
||||
let notification = ThreadRealtimeErrorNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
@@ -338,7 +263,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
reason,
|
||||
grant_root,
|
||||
};
|
||||
let (_pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ApplyPatchApproval(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
@@ -382,7 +307,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
reason,
|
||||
grant_root,
|
||||
};
|
||||
let (pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::FileChangeRequestApproval(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
@@ -391,7 +316,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id,
|
||||
item_id,
|
||||
patch_changes,
|
||||
pending_request_id,
|
||||
rx,
|
||||
conversation,
|
||||
outgoing,
|
||||
@@ -438,7 +362,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
reason,
|
||||
parsed_cmd,
|
||||
};
|
||||
let (_pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ExecCommandApproval(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
@@ -511,7 +435,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
proposed_network_policy_amendments: proposed_network_policy_amendments_v2,
|
||||
available_decisions: Some(available_decisions),
|
||||
};
|
||||
let (pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
|
||||
params,
|
||||
))
|
||||
@@ -523,7 +447,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
approval_id,
|
||||
call_id,
|
||||
completion_item,
|
||||
pending_request_id,
|
||||
rx,
|
||||
conversation,
|
||||
outgoing,
|
||||
@@ -566,16 +489,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
item_id: request.call_id,
|
||||
questions,
|
||||
};
|
||||
let (pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ToolRequestUserInput(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_request_user_input_response(
|
||||
event_turn_id,
|
||||
pending_request_id,
|
||||
rx,
|
||||
conversation,
|
||||
thread_state,
|
||||
user_input_guard,
|
||||
)
|
||||
.await;
|
||||
@@ -629,7 +550,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
tool: tool.clone(),
|
||||
arguments: arguments.clone(),
|
||||
};
|
||||
let (_pending_request_id, rx) = outgoing
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::DynamicToolCall(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
@@ -1215,7 +1136,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
// Until we migrate the core to be aware of a first class FileChangeItem
|
||||
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
|
||||
let item_id = patch_begin_event.call_id.clone();
|
||||
let changes = convert_patch_changes(&patch_begin_event.changes);
|
||||
|
||||
let first_start = {
|
||||
let mut state = thread_state.lock().await;
|
||||
@@ -1227,7 +1147,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
if first_start {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: item_id.clone(),
|
||||
changes,
|
||||
changes: convert_patch_changes(&patch_begin_event.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
let notification = ItemStartedNotification {
|
||||
@@ -1409,8 +1329,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
// If this is a TurnAborted, reply to any pending interrupt requests.
|
||||
EventMsg::TurnAborted(turn_aborted_event) => {
|
||||
// All per-thread requests are bound to a turn, so abort them.
|
||||
outgoing.abort_pending_server_requests().await;
|
||||
let pending = {
|
||||
let mut state = thread_state.lock().await;
|
||||
std::mem::take(&mut state.pending_interrupts)
|
||||
@@ -1807,7 +1725,6 @@ async fn on_patch_approval_response(
|
||||
let response = receiver.await;
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
if let Err(submit_err) = codex
|
||||
@@ -1864,7 +1781,6 @@ async fn on_exec_approval_response(
|
||||
let response = receiver.await;
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
return;
|
||||
@@ -1900,18 +1816,14 @@ async fn on_exec_approval_response(
|
||||
|
||||
async fn on_request_user_input_response(
|
||||
event_turn_id: String,
|
||||
pending_request_id: RequestId,
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
conversation: Arc<CodexThread>,
|
||||
thread_state: Arc<Mutex<ThreadState>>,
|
||||
user_input_guard: ThreadWatchActiveGuard,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
drop(user_input_guard);
|
||||
let value = match response {
|
||||
Ok(Ok(value)) => value,
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
let empty = CoreRequestUserInputResponse {
|
||||
@@ -2022,7 +1934,6 @@ async fn on_file_change_request_approval_response(
|
||||
conversation_id: ThreadId,
|
||||
item_id: String,
|
||||
changes: Vec<FileUpdateChange>,
|
||||
pending_request_id: RequestId,
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
codex: Arc<CodexThread>,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
@@ -2030,7 +1941,6 @@ async fn on_file_change_request_approval_response(
|
||||
permission_guard: ThreadWatchActiveGuard,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
drop(permission_guard);
|
||||
let (decision, completion_status) = match response {
|
||||
Ok(Ok(value)) => {
|
||||
@@ -2048,7 +1958,6 @@ async fn on_file_change_request_approval_response(
|
||||
// Only short-circuit on declines/cancels/failures.
|
||||
(decision, completion_status)
|
||||
}
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
(ReviewDecision::Denied, Some(PatchApplyStatus::Failed))
|
||||
@@ -2090,7 +1999,6 @@ async fn on_command_execution_request_approval_response(
|
||||
approval_id: Option<String>,
|
||||
item_id: String,
|
||||
completion_item: Option<CommandExecutionCompletionItem>,
|
||||
pending_request_id: RequestId,
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
conversation: Arc<CodexThread>,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
@@ -2098,7 +2006,6 @@ async fn on_command_execution_request_approval_response(
|
||||
permission_guard: ThreadWatchActiveGuard,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
resolve_server_request_on_thread_listener(&thread_state, pending_request_id).await;
|
||||
drop(permission_guard);
|
||||
let (decision, completion_status) = match response {
|
||||
Ok(Ok(value)) => {
|
||||
@@ -2150,7 +2057,6 @@ async fn on_command_execution_request_approval_response(
|
||||
};
|
||||
(decision, completion_status)
|
||||
}
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
(ReviewDecision::Denied, Some(CommandExecutionStatus::Failed))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ use tokio::sync::oneshot;
|
||||
use tracing::error;
|
||||
|
||||
use crate::outgoing_message::ClientRequestResult;
|
||||
use crate::server_request_error::is_turn_transition_server_request_error;
|
||||
|
||||
pub(crate) async fn on_call_response(
|
||||
call_id: String,
|
||||
@@ -19,7 +18,6 @@ pub(crate) async fn on_call_response(
|
||||
let response = receiver.await;
|
||||
let (response, _error) = match response {
|
||||
Ok(Ok(value)) => decode_response(value),
|
||||
Ok(Err(err)) if is_turn_transition_server_request_error(&err) => return,
|
||||
Ok(Err(err)) => {
|
||||
error!("request failed with client error: {err:?}");
|
||||
fallback_response("dynamic tool request failed")
|
||||
|
||||
@@ -52,7 +52,6 @@ use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::registry::Registry;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
mod app_server_tracing;
|
||||
mod bespoke_event_handling;
|
||||
mod codex_message_processor;
|
||||
mod config_api;
|
||||
@@ -64,7 +63,6 @@ mod fuzzy_file_search;
|
||||
mod message_processor;
|
||||
mod models;
|
||||
mod outgoing_message;
|
||||
mod server_request_error;
|
||||
mod thread_state;
|
||||
mod thread_status;
|
||||
mod transport;
|
||||
@@ -448,7 +446,7 @@ pub async fn run_main_with_transport(
|
||||
let otel = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
Some("codex-app-server"),
|
||||
Some("codex_app_server"),
|
||||
default_analytics_enabled,
|
||||
)
|
||||
.map_err(|e| {
|
||||
@@ -558,6 +556,7 @@ pub async fn run_main_with_transport(
|
||||
outgoing: outgoing_message_sender,
|
||||
arg0_paths,
|
||||
config: Arc::new(config),
|
||||
single_client_mode,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
@@ -675,7 +674,6 @@ pub async fn run_main_with_transport(
|
||||
.process_request(
|
||||
connection_id,
|
||||
request,
|
||||
transport,
|
||||
&mut connection_state.session,
|
||||
&connection_state.outbound_initialized,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::external_agent_config_api::ExternalAgentConfigApi;
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::transport::AppServerTransport;
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
@@ -60,7 +59,6 @@ use tokio::sync::watch;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::Instrument;
|
||||
|
||||
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
@@ -143,13 +141,13 @@ pub(crate) struct ConnectionSessionState {
|
||||
pub(crate) experimental_api_enabled: bool,
|
||||
pub(crate) opted_out_notification_methods: HashSet<String>,
|
||||
pub(crate) app_server_client_name: Option<String>,
|
||||
pub(crate) client_version: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct MessageProcessorArgs {
|
||||
pub(crate) outgoing: Arc<OutgoingMessageSender>,
|
||||
pub(crate) arg0_paths: Arg0DispatchPaths,
|
||||
pub(crate) config: Arc<Config>,
|
||||
pub(crate) single_client_mode: bool,
|
||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||
pub(crate) loader_overrides: LoaderOverrides,
|
||||
pub(crate) cloud_requirements: CloudRequirementsLoader,
|
||||
@@ -165,6 +163,7 @@ impl MessageProcessor {
|
||||
outgoing,
|
||||
arg0_paths,
|
||||
config,
|
||||
single_client_mode,
|
||||
cli_overrides,
|
||||
loader_overrides,
|
||||
cloud_requirements,
|
||||
@@ -200,6 +199,7 @@ impl MessageProcessor {
|
||||
config: Arc::clone(&config),
|
||||
cli_overrides: cli_overrides.clone(),
|
||||
cloud_requirements: cloud_requirements.clone(),
|
||||
single_client_mode,
|
||||
feedback,
|
||||
});
|
||||
let config_api = ConfigApi::new(
|
||||
@@ -224,50 +224,46 @@ impl MessageProcessor {
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
request: JSONRPCRequest,
|
||||
transport: AppServerTransport,
|
||||
session: &mut ConnectionSessionState,
|
||||
outbound_initialized: &AtomicBool,
|
||||
) {
|
||||
let request_span =
|
||||
crate::app_server_tracing::request_span(&request, transport, connection_id, session);
|
||||
async {
|
||||
let request_method = request.method.as_str();
|
||||
tracing::trace!(
|
||||
?connection_id,
|
||||
request_id = ?request.id,
|
||||
"app-server request: {request_method}"
|
||||
);
|
||||
let request_id = ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id: request.id.clone(),
|
||||
};
|
||||
let request_json = match serde_json::to_value(&request) {
|
||||
Ok(request_json) => request_json,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let request_method = request.method.as_str();
|
||||
tracing::trace!(
|
||||
?connection_id,
|
||||
request_id = ?request.id,
|
||||
"app-server request: {request_method}"
|
||||
);
|
||||
let request_id = ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id: request.id.clone(),
|
||||
};
|
||||
let request_json = match serde_json::to_value(&request) {
|
||||
Ok(request_json) => request_json,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let codex_request = match serde_json::from_value::<ClientRequest>(request_json) {
|
||||
Ok(codex_request) => codex_request,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let codex_request = match serde_json::from_value::<ClientRequest>(request_json) {
|
||||
Ok(codex_request) => codex_request,
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: format!("Invalid request: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match codex_request {
|
||||
match codex_request {
|
||||
// Handle Initialize internally so CodexMessageProcessor does not have to concern
|
||||
// itself with the `initialized` bool.
|
||||
ClientRequest::Initialize { request_id, params } => {
|
||||
@@ -308,8 +304,6 @@ impl MessageProcessor {
|
||||
title: _title,
|
||||
version,
|
||||
} = params.client_info;
|
||||
session.app_server_client_name = Some(name.clone());
|
||||
session.client_version = Some(version.clone());
|
||||
if let Err(error) = set_default_originator(name.clone()) {
|
||||
match error {
|
||||
SetOriginatorError::InvalidHeaderValue => {
|
||||
@@ -336,6 +330,7 @@ impl MessageProcessor {
|
||||
if let Ok(mut suffix) = USER_AGENT_SUFFIX.lock() {
|
||||
*suffix = Some(user_agent_suffix);
|
||||
}
|
||||
session.app_server_client_name = Some(name.clone());
|
||||
|
||||
let user_agent = get_codex_user_agent();
|
||||
let response = InitializeResponse { user_agent };
|
||||
@@ -343,9 +338,6 @@ impl MessageProcessor {
|
||||
|
||||
session.initialized = true;
|
||||
outbound_initialized.store(true, Ordering::Release);
|
||||
self.codex_message_processor
|
||||
.connection_initialized(connection_id)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -360,97 +352,91 @@ impl MessageProcessor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reason) = codex_request.experimental_reason()
|
||||
&& !session.experimental_api_enabled
|
||||
{
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: experimental_required_message(reason),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
match codex_request {
|
||||
ClientRequest::ConfigRead { request_id, params } => {
|
||||
self.handle_config_read(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigDetect { request_id, params } => {
|
||||
self.handle_external_agent_config_detect(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigImport { request_id, params } => {
|
||||
self.handle_external_agent_config_import(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigValueWrite { request_id, params } => {
|
||||
self.handle_config_value_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigBatchWrite { request_id, params } => {
|
||||
self.handle_config_batch_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigRequirementsRead {
|
||||
request_id,
|
||||
params: _,
|
||||
} => {
|
||||
self.handle_config_requirements_read(ConnectionRequestId {
|
||||
if let Some(reason) = codex_request.experimental_reason()
|
||||
&& !session.experimental_api_enabled
|
||||
{
|
||||
let error = JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: experimental_required_message(reason),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
|
||||
match codex_request {
|
||||
ClientRequest::ConfigRead { request_id, params } => {
|
||||
self.handle_config_read(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
})
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigDetect { request_id, params } => {
|
||||
self.handle_external_agent_config_detect(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ExternalAgentConfigImport { request_id, params } => {
|
||||
self.handle_external_agent_config_import(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigValueWrite { request_id, params } => {
|
||||
self.handle_config_value_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigBatchWrite { request_id, params } => {
|
||||
self.handle_config_batch_write(
|
||||
ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
},
|
||||
params,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ConfigRequirementsRead {
|
||||
request_id,
|
||||
params: _,
|
||||
} => {
|
||||
self.handle_config_requirements_read(ConnectionRequestId {
|
||||
connection_id,
|
||||
request_id,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
other => {
|
||||
// Box the delegated future so this wrapper's async state machine does not
|
||||
// inline the full `CodexMessageProcessor::process_request` future, which
|
||||
// can otherwise push worker-thread stack usage over the edge.
|
||||
self.codex_message_processor
|
||||
.process_request(connection_id, other, session.app_server_client_name.clone())
|
||||
.boxed()
|
||||
.await;
|
||||
}
|
||||
other => {
|
||||
// Box the delegated future so this wrapper's async state machine does not
|
||||
// inline the full `CodexMessageProcessor::process_request` future, which
|
||||
// can otherwise push worker-thread stack usage over the edge.
|
||||
self.codex_message_processor
|
||||
.process_request(
|
||||
connection_id,
|
||||
other,
|
||||
session.app_server_client_name.clone(),
|
||||
)
|
||||
.boxed()
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
.instrument(request_span)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn process_notification(&self, notification: JSONRPCNotification) {
|
||||
|
||||
@@ -17,7 +17,6 @@ use tokio::sync::oneshot;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::error_code::INTERNAL_ERROR_CODE;
|
||||
use crate::server_request_error::TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON;
|
||||
|
||||
#[cfg(test)]
|
||||
use codex_protocol::account::PlanType;
|
||||
@@ -63,7 +62,6 @@ pub(crate) struct ThreadScopedOutgoingMessageSender {
|
||||
struct PendingCallbackEntry {
|
||||
callback: oneshot::Sender<ClientRequestResult>,
|
||||
thread_id: Option<ThreadId>,
|
||||
request: ServerRequest,
|
||||
}
|
||||
|
||||
impl ThreadScopedOutgoingMessageSender {
|
||||
@@ -82,12 +80,12 @@ impl ThreadScopedOutgoingMessageSender {
|
||||
pub(crate) async fn send_request(
|
||||
&self,
|
||||
payload: ServerRequestPayload,
|
||||
) -> (RequestId, oneshot::Receiver<ClientRequestResult>) {
|
||||
) -> oneshot::Receiver<ClientRequestResult> {
|
||||
self.outgoing
|
||||
.send_request_to_connections(
|
||||
Some(self.connection_ids.as_slice()),
|
||||
.send_request_to_thread_connections(
|
||||
self.thread_id,
|
||||
self.connection_ids.as_slice(),
|
||||
payload,
|
||||
Some(self.thread_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -101,20 +99,6 @@ impl ThreadScopedOutgoingMessageSender {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn abort_pending_server_requests(&self) {
|
||||
self.outgoing
|
||||
.cancel_requests_for_thread(
|
||||
self.thread_id,
|
||||
Some(JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "client request resolved because the turn state was changed"
|
||||
.to_string(),
|
||||
data: Some(serde_json::json!({ "reason": TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON })),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn send_response<T: Serialize>(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -145,23 +129,38 @@ impl OutgoingMessageSender {
|
||||
&self,
|
||||
request: ServerRequestPayload,
|
||||
) -> (RequestId, oneshot::Receiver<ClientRequestResult>) {
|
||||
self.send_request_to_connections(None, request, None).await
|
||||
self.send_request_with_id_to_connections(&[], request, None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn send_request_to_thread_connections(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
connection_ids: &[ConnectionId],
|
||||
request: ServerRequestPayload,
|
||||
) -> oneshot::Receiver<ClientRequestResult> {
|
||||
if connection_ids.is_empty() {
|
||||
let (_tx, rx) = oneshot::channel();
|
||||
return rx;
|
||||
}
|
||||
let (_request_id, receiver) = self
|
||||
.send_request_with_id_to_connections(connection_ids, request, Some(thread_id))
|
||||
.await;
|
||||
receiver
|
||||
}
|
||||
|
||||
fn next_request_id(&self) -> RequestId {
|
||||
RequestId::Integer(self.next_server_request_id.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
async fn send_request_to_connections(
|
||||
async fn send_request_with_id_to_connections(
|
||||
&self,
|
||||
connection_ids: Option<&[ConnectionId]>,
|
||||
connection_ids: &[ConnectionId],
|
||||
request: ServerRequestPayload,
|
||||
thread_id: Option<ThreadId>,
|
||||
) -> (RequestId, oneshot::Receiver<ClientRequestResult>) {
|
||||
let id = self.next_request_id();
|
||||
let outgoing_message_id = id.clone();
|
||||
let request = request.request_with_id(outgoing_message_id.clone());
|
||||
|
||||
let (tx_approve, rx_approve) = oneshot::channel();
|
||||
{
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
@@ -170,39 +169,36 @@ impl OutgoingMessageSender {
|
||||
PendingCallbackEntry {
|
||||
callback: tx_approve,
|
||||
thread_id,
|
||||
request: request.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let outgoing_message = OutgoingMessage::Request(request);
|
||||
let send_result = match connection_ids {
|
||||
None => {
|
||||
self.sender
|
||||
.send(OutgoingEnvelope::Broadcast {
|
||||
message: outgoing_message,
|
||||
let outgoing_message =
|
||||
OutgoingMessage::Request(request.request_with_id(outgoing_message_id.clone()));
|
||||
let send_result = if connection_ids.is_empty() {
|
||||
self.sender
|
||||
.send(OutgoingEnvelope::Broadcast {
|
||||
message: outgoing_message,
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
let mut send_error = None;
|
||||
for connection_id in connection_ids {
|
||||
if let Err(err) = self
|
||||
.sender
|
||||
.send(OutgoingEnvelope::ToConnection {
|
||||
connection_id: *connection_id,
|
||||
message: outgoing_message.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
send_error = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(connection_ids) => {
|
||||
let mut send_error = None;
|
||||
for connection_id in connection_ids {
|
||||
if let Err(err) = self
|
||||
.sender
|
||||
.send(OutgoingEnvelope::ToConnection {
|
||||
connection_id: *connection_id,
|
||||
message: outgoing_message.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
send_error = Some(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
match send_error {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(()),
|
||||
}
|
||||
match send_error {
|
||||
Some(err) => Err(err),
|
||||
None => Ok(()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,28 +210,11 @@ impl OutgoingMessageSender {
|
||||
(outgoing_message_id, rx_approve)
|
||||
}
|
||||
|
||||
pub(crate) async fn replay_requests_to_connection_for_thread(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
thread_id: ThreadId,
|
||||
) {
|
||||
let requests = self.pending_requests_for_thread(thread_id).await;
|
||||
for request in requests {
|
||||
if let Err(err) = self
|
||||
.sender
|
||||
.send(OutgoingEnvelope::ToConnection {
|
||||
connection_id,
|
||||
message: OutgoingMessage::Request(request),
|
||||
})
|
||||
.await
|
||||
{
|
||||
warn!("failed to resend request to client: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn notify_client_response(&self, id: RequestId, result: Result) {
|
||||
let entry = self.take_request_callback(&id).await;
|
||||
let entry = {
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
request_id_to_callback.remove_entry(&id)
|
||||
};
|
||||
|
||||
match entry {
|
||||
Some((id, entry)) => {
|
||||
@@ -250,7 +229,10 @@ impl OutgoingMessageSender {
|
||||
}
|
||||
|
||||
pub(crate) async fn notify_client_error(&self, id: RequestId, error: JSONRPCErrorError) {
|
||||
let entry = self.take_request_callback(&id).await;
|
||||
let entry = {
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
request_id_to_callback.remove_entry(&id)
|
||||
};
|
||||
|
||||
match entry {
|
||||
Some((id, entry)) => {
|
||||
@@ -266,62 +248,23 @@ impl OutgoingMessageSender {
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel_request(&self, id: &RequestId) -> bool {
|
||||
self.take_request_callback(id).await.is_some()
|
||||
let entry = {
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
request_id_to_callback.remove_entry(id)
|
||||
};
|
||||
entry.is_some()
|
||||
}
|
||||
|
||||
async fn take_request_callback(
|
||||
&self,
|
||||
id: &RequestId,
|
||||
) -> Option<(RequestId, PendingCallbackEntry)> {
|
||||
pub(crate) async fn cancel_requests_for_thread(&self, thread_id: ThreadId) {
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
request_id_to_callback.remove_entry(id)
|
||||
}
|
||||
|
||||
pub(crate) async fn pending_requests_for_thread(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> Vec<ServerRequest> {
|
||||
let request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
let mut requests = request_id_to_callback
|
||||
let request_ids = request_id_to_callback
|
||||
.iter()
|
||||
.filter_map(|(_, entry)| {
|
||||
(entry.thread_id == Some(thread_id)).then_some(entry.request.clone())
|
||||
.filter_map(|(request_id, entry)| {
|
||||
(entry.thread_id == Some(thread_id)).then_some(request_id.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
requests.sort_by(|left, right| left.id().cmp(right.id()));
|
||||
requests
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel_requests_for_thread(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
error: Option<JSONRPCErrorError>,
|
||||
) {
|
||||
let entries = {
|
||||
let mut request_id_to_callback = self.request_id_to_callback.lock().await;
|
||||
let request_ids = request_id_to_callback
|
||||
.iter()
|
||||
.filter_map(|(request_id, entry)| {
|
||||
(entry.thread_id == Some(thread_id)).then_some(request_id.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut entries = Vec::with_capacity(request_ids.len());
|
||||
for request_id in request_ids {
|
||||
if let Some(entry) = request_id_to_callback.remove(&request_id) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
entries
|
||||
};
|
||||
|
||||
if let Some(error) = error {
|
||||
for entry in entries {
|
||||
if let Err(err) = entry.callback.send(Err(error.clone())) {
|
||||
let request_id = entry.request.id();
|
||||
warn!("could not notify callback for {request_id:?} due to: {err:?}",);
|
||||
}
|
||||
}
|
||||
for request_id in request_ids {
|
||||
request_id_to_callback.remove(&request_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,18 +441,14 @@ mod tests {
|
||||
use codex_app_server_protocol::ApplyPatchApprovalParams;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::DynamicToolCallParams;
|
||||
use codex_app_server_protocol::FileChangeRequestApprovalParams;
|
||||
use codex_app_server_protocol::LoginChatGptCompleteNotification;
|
||||
use codex_app_server_protocol::ModelRerouteReason;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RateLimitWindow;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_protocol::ThreadId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -612,7 +551,6 @@ mod tests {
|
||||
fn verify_account_updated_notification_serialization() {
|
||||
let notification = ServerNotification::AccountUpdated(AccountUpdatedNotification {
|
||||
auth_mode: Some(AuthMode::ApiKey),
|
||||
plan_type: None,
|
||||
});
|
||||
|
||||
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
|
||||
@@ -620,8 +558,7 @@ mod tests {
|
||||
json!({
|
||||
"method": "account/updated",
|
||||
"params": {
|
||||
"authMode": "apikey",
|
||||
"planType": null
|
||||
"authMode": "apikey"
|
||||
},
|
||||
}),
|
||||
serde_json::to_value(jsonrpc_notification)
|
||||
@@ -786,121 +723,4 @@ mod tests {
|
||||
.expect("waiter should receive a callback");
|
||||
assert_eq!(result, Err(error));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_requests_for_thread_returns_thread_requests_in_request_id_order() {
|
||||
let (tx, _rx) = mpsc::channel::<OutgoingEnvelope>(8);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
|
||||
let thread_id = ThreadId::new();
|
||||
let thread_outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing.clone(),
|
||||
vec![ConnectionId(1)],
|
||||
thread_id,
|
||||
);
|
||||
|
||||
let (dynamic_tool_request_id, _dynamic_tool_waiter) = thread_outgoing
|
||||
.send_request(ServerRequestPayload::DynamicToolCall(
|
||||
DynamicToolCallParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-0".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({}),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let (first_request_id, _first_waiter) = thread_outgoing
|
||||
.send_request(ServerRequestPayload::ToolRequestUserInput(
|
||||
ToolRequestUserInputParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
questions: vec![],
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let (second_request_id, _second_waiter) = thread_outgoing
|
||||
.send_request(ServerRequestPayload::FileChangeRequestApproval(
|
||||
FileChangeRequestApprovalParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-2".to_string(),
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let pending_requests = outgoing.pending_requests_for_thread(thread_id).await;
|
||||
assert_eq!(
|
||||
pending_requests
|
||||
.iter()
|
||||
.map(ServerRequest::id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
&dynamic_tool_request_id,
|
||||
&first_request_id,
|
||||
&second_request_id
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_requests_for_thread_cancels_all_thread_requests() {
|
||||
let (tx, _rx) = mpsc::channel::<OutgoingEnvelope>(8);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
|
||||
let thread_id = ThreadId::new();
|
||||
let thread_outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing.clone(),
|
||||
vec![ConnectionId(1)],
|
||||
thread_id,
|
||||
);
|
||||
|
||||
let (_dynamic_tool_request_id, dynamic_tool_waiter) = thread_outgoing
|
||||
.send_request(ServerRequestPayload::DynamicToolCall(
|
||||
DynamicToolCallParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-0".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({}),
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let (_request_id, user_input_waiter) = thread_outgoing
|
||||
.send_request(ServerRequestPayload::ToolRequestUserInput(
|
||||
ToolRequestUserInputParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "call-1".to_string(),
|
||||
questions: vec![],
|
||||
},
|
||||
))
|
||||
.await;
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: "tracked request cancelled".to_string(),
|
||||
data: None,
|
||||
};
|
||||
|
||||
outgoing
|
||||
.cancel_requests_for_thread(thread_id, Some(error.clone()))
|
||||
.await;
|
||||
|
||||
let dynamic_tool_result = timeout(Duration::from_secs(1), dynamic_tool_waiter)
|
||||
.await
|
||||
.expect("dynamic tool waiter should resolve")
|
||||
.expect("dynamic tool waiter should receive a callback");
|
||||
let user_input_result = timeout(Duration::from_secs(1), user_input_waiter)
|
||||
.await
|
||||
.expect("user input waiter should resolve")
|
||||
.expect("user input waiter should receive a callback");
|
||||
assert_eq!(dynamic_tool_result, Err(error.clone()));
|
||||
assert_eq!(user_input_result, Err(error));
|
||||
assert!(
|
||||
outgoing
|
||||
.pending_requests_for_thread(thread_id)
|
||||
.await
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
pub(crate) const TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON: &str = "turnTransition";
|
||||
|
||||
pub(crate) fn is_turn_transition_server_request_error(error: &JSONRPCErrorError) -> bool {
|
||||
error
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|data| data.get("reason"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(TURN_TRANSITION_PENDING_REQUEST_ERROR_REASON)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_turn_transition_server_request_error;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn turn_transition_error_is_detected() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: -1,
|
||||
message: "client request resolved because the turn state was changed".to_string(),
|
||||
data: Some(json!({ "reason": "turnTransition" })),
|
||||
};
|
||||
|
||||
assert_eq!(is_turn_transition_server_request_error(&error), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_error_is_not_detected() {
|
||||
let error = JSONRPCErrorError {
|
||||
code: -1,
|
||||
message: "boom".to_string(),
|
||||
data: Some(json!({ "reason": "other" })),
|
||||
};
|
||||
|
||||
assert_eq!(is_turn_transition_server_request_error(&error), false);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::outgoing_message::ConnectionId;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnError;
|
||||
@@ -29,16 +28,8 @@ pub(crate) struct PendingThreadResumeRequest {
|
||||
pub(crate) config_snapshot: ThreadConfigSnapshot,
|
||||
}
|
||||
|
||||
// ThreadListenerCommand is used to perform operations in the context of the thread listener, for serialization purposes.
|
||||
pub(crate) enum ThreadListenerCommand {
|
||||
// SendThreadResumeResponse is used to resume an already running thread by sending the thread's history to the client and atomically subscribing for new updates.
|
||||
SendThreadResumeResponse(Box<PendingThreadResumeRequest>),
|
||||
// ResolveServerRequest is used to notify the client that the request has been resolved.
|
||||
// It is executed in the thread listener's context to ensure that the resolved notification is ordered with regard to the request itself.
|
||||
ResolveServerRequest {
|
||||
request_id: RequestId,
|
||||
completion_tx: oneshot::Sender<()>,
|
||||
},
|
||||
SendThreadResumeResponse(PendingThreadResumeRequest),
|
||||
}
|
||||
|
||||
/// Per-conversation accumulation of the latest states e.g. error message while a turn runs.
|
||||
@@ -60,6 +51,7 @@ pub(crate) struct ThreadState {
|
||||
listener_command_tx: Option<mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
current_turn_history: ThreadHistoryBuilder,
|
||||
listener_thread: Option<Weak<CodexThread>>,
|
||||
subscribed_connections: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
impl ThreadState {
|
||||
@@ -94,6 +86,18 @@ impl ThreadState {
|
||||
self.listener_thread = None;
|
||||
}
|
||||
|
||||
pub(crate) fn add_connection(&mut self, connection_id: ConnectionId) {
|
||||
self.subscribed_connections.insert(connection_id);
|
||||
}
|
||||
|
||||
pub(crate) fn remove_connection(&mut self, connection_id: ConnectionId) {
|
||||
self.subscribed_connections.remove(&connection_id);
|
||||
}
|
||||
|
||||
pub(crate) fn subscribed_connection_ids(&self) -> Vec<ConnectionId> {
|
||||
self.subscribed_connections.iter().copied().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_experimental_raw_events(&mut self, enabled: bool) {
|
||||
self.experimental_raw_events = enabled;
|
||||
}
|
||||
@@ -122,112 +126,55 @@ struct SubscriptionState {
|
||||
connection_id: ConnectionId,
|
||||
}
|
||||
|
||||
struct ThreadEntry {
|
||||
state: Arc<Mutex<ThreadState>>,
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
impl Default for ThreadEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(ThreadState::default())),
|
||||
connection_ids: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ThreadStateManagerInner {
|
||||
live_connections: HashSet<ConnectionId>,
|
||||
threads: HashMap<ThreadId, ThreadEntry>,
|
||||
pub(crate) struct ThreadStateManager {
|
||||
thread_states: HashMap<ThreadId, Arc<Mutex<ThreadState>>>,
|
||||
subscription_state_by_id: HashMap<Uuid, SubscriptionState>,
|
||||
thread_ids_by_connection: HashMap<ConnectionId, HashSet<ThreadId>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct ThreadStateManager {
|
||||
state: Arc<Mutex<ThreadStateManagerInner>>,
|
||||
}
|
||||
|
||||
impl ThreadStateManager {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) async fn connection_initialized(&self, connection_id: ConnectionId) {
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.live_connections
|
||||
.insert(connection_id);
|
||||
pub(crate) fn thread_state(&mut self, thread_id: ThreadId) -> Arc<Mutex<ThreadState>> {
|
||||
self.thread_states
|
||||
.entry(thread_id)
|
||||
.or_insert_with(|| Arc::new(Mutex::new(ThreadState::default())))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec<ConnectionId> {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.map(|thread_entry| thread_entry.connection_ids.iter().copied().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_state(&self, thread_id: ThreadId) -> Arc<Mutex<ThreadState>> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.threads.entry(thread_id).or_default().state.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_listener(&self, subscription_id: Uuid) -> Option<ThreadId> {
|
||||
let (subscription_state, connection_still_subscribed_to_thread, thread_state) = {
|
||||
let mut state = self.state.lock().await;
|
||||
let subscription_state = state.subscription_state_by_id.remove(&subscription_id)?;
|
||||
let thread_id = subscription_state.thread_id;
|
||||
|
||||
let connection_still_subscribed_to_thread = state
|
||||
.subscription_state_by_id
|
||||
.values()
|
||||
.any(|subscription_state_entry| {
|
||||
subscription_state_entry.thread_id == thread_id
|
||||
&& subscription_state_entry.connection_id
|
||||
== subscription_state.connection_id
|
||||
});
|
||||
if !connection_still_subscribed_to_thread {
|
||||
let mut remove_connection_entry = false;
|
||||
if let Some(thread_ids) = state
|
||||
.thread_ids_by_connection
|
||||
.get_mut(&subscription_state.connection_id)
|
||||
{
|
||||
thread_ids.remove(&thread_id);
|
||||
remove_connection_entry = thread_ids.is_empty();
|
||||
}
|
||||
if remove_connection_entry {
|
||||
state
|
||||
.thread_ids_by_connection
|
||||
.remove(&subscription_state.connection_id);
|
||||
}
|
||||
if let Some(thread_entry) = state.threads.get_mut(&thread_id) {
|
||||
thread_entry
|
||||
.connection_ids
|
||||
.remove(&subscription_state.connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
let thread_state = state.threads.get(&thread_id).map(|thread_entry| {
|
||||
(
|
||||
thread_entry.connection_ids.is_empty(),
|
||||
thread_entry.state.clone(),
|
||||
)
|
||||
});
|
||||
(
|
||||
subscription_state,
|
||||
connection_still_subscribed_to_thread,
|
||||
thread_state,
|
||||
)
|
||||
};
|
||||
pub(crate) async fn remove_listener(&mut self, subscription_id: Uuid) -> Option<ThreadId> {
|
||||
let subscription_state = self.subscription_state_by_id.remove(&subscription_id)?;
|
||||
let thread_id = subscription_state.thread_id;
|
||||
|
||||
if let Some((no_subscribers, thread_state)) = thread_state {
|
||||
let thread_state = thread_state.lock().await;
|
||||
if !connection_still_subscribed_to_thread && no_subscribers {
|
||||
let connection_still_subscribed_to_thread =
|
||||
self.subscription_state_by_id.values().any(|state| {
|
||||
state.thread_id == thread_id
|
||||
&& state.connection_id == subscription_state.connection_id
|
||||
});
|
||||
if !connection_still_subscribed_to_thread {
|
||||
let mut remove_connection_entry = false;
|
||||
if let Some(thread_ids) = self
|
||||
.thread_ids_by_connection
|
||||
.get_mut(&subscription_state.connection_id)
|
||||
{
|
||||
thread_ids.remove(&thread_id);
|
||||
remove_connection_entry = thread_ids.is_empty();
|
||||
}
|
||||
if remove_connection_entry {
|
||||
self.thread_ids_by_connection
|
||||
.remove(&subscription_state.connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(thread_state) = self.thread_states.get(&thread_id) {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if !connection_still_subscribed_to_thread {
|
||||
thread_state.remove_connection(subscription_state.connection_id);
|
||||
}
|
||||
if thread_state.subscribed_connection_ids().is_empty() {
|
||||
tracing::debug!(
|
||||
thread_id = %thread_id,
|
||||
subscription_id = %subscription_id,
|
||||
@@ -240,24 +187,8 @@ impl ThreadStateManager {
|
||||
Some(thread_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_thread_state(&self, thread_id: ThreadId) {
|
||||
let thread_state = {
|
||||
let mut state = self.state.lock().await;
|
||||
let thread_state = state
|
||||
.threads
|
||||
.remove(&thread_id)
|
||||
.map(|thread_entry| thread_entry.state);
|
||||
state
|
||||
.subscription_state_by_id
|
||||
.retain(|_, state| state.thread_id != thread_id);
|
||||
state.thread_ids_by_connection.retain(|_, thread_ids| {
|
||||
thread_ids.remove(&thread_id);
|
||||
!thread_ids.is_empty()
|
||||
});
|
||||
thread_state
|
||||
};
|
||||
|
||||
if let Some(thread_state) = thread_state {
|
||||
pub(crate) async fn remove_thread_state(&mut self, thread_id: ThreadId) {
|
||||
if let Some(thread_state) = self.thread_states.remove(&thread_id) {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
tracing::debug!(
|
||||
thread_id = %thread_id,
|
||||
@@ -268,189 +199,142 @@ impl ThreadStateManager {
|
||||
);
|
||||
thread_state.clear_listener();
|
||||
}
|
||||
self.subscription_state_by_id
|
||||
.retain(|_, state| state.thread_id != thread_id);
|
||||
self.thread_ids_by_connection.retain(|_, thread_ids| {
|
||||
thread_ids.remove(&thread_id);
|
||||
!thread_ids.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) async fn unsubscribe_connection_from_thread(
|
||||
&self,
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
connection_id: ConnectionId,
|
||||
) -> bool {
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
if !state.threads.contains_key(&thread_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !state
|
||||
.thread_ids_by_connection
|
||||
.get(&connection_id)
|
||||
.is_some_and(|thread_ids| thread_ids.contains(&thread_id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(thread_ids) = state.thread_ids_by_connection.get_mut(&connection_id) {
|
||||
thread_ids.remove(&thread_id);
|
||||
if thread_ids.is_empty() {
|
||||
state.thread_ids_by_connection.remove(&connection_id);
|
||||
}
|
||||
}
|
||||
if let Some(thread_entry) = state.threads.get_mut(&thread_id) {
|
||||
thread_entry.connection_ids.remove(&connection_id);
|
||||
}
|
||||
|
||||
state
|
||||
.subscription_state_by_id
|
||||
.retain(|_, subscription_state| {
|
||||
!(subscription_state.thread_id == thread_id
|
||||
&& subscription_state.connection_id == connection_id)
|
||||
});
|
||||
let Some(thread_state) = self.thread_states.get(&thread_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if !self
|
||||
.thread_ids_by_connection
|
||||
.get(&connection_id)
|
||||
.is_some_and(|thread_ids| thread_ids.contains(&thread_id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(thread_ids) = self.thread_ids_by_connection.get_mut(&connection_id) {
|
||||
thread_ids.remove(&thread_id);
|
||||
if thread_ids.is_empty() {
|
||||
self.thread_ids_by_connection.remove(&connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.subscription_state_by_id.retain(|_, state| {
|
||||
!(state.thread_id == thread_id && state.connection_id == connection_id)
|
||||
});
|
||||
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.remove_connection(connection_id);
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) async fn has_subscribers(&self, thread_id: ThreadId) -> bool {
|
||||
self.state
|
||||
let Some(thread_state) = self.thread_states.get(&thread_id) else {
|
||||
return false;
|
||||
};
|
||||
!thread_state
|
||||
.lock()
|
||||
.await
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.is_some_and(|thread_entry| !thread_entry.connection_ids.is_empty())
|
||||
.subscribed_connection_ids()
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_listener(
|
||||
&self,
|
||||
&mut self,
|
||||
subscription_id: Uuid,
|
||||
thread_id: ThreadId,
|
||||
connection_id: ConnectionId,
|
||||
experimental_raw_events: bool,
|
||||
) -> Arc<Mutex<ThreadState>> {
|
||||
let thread_state = {
|
||||
let mut state = self.state.lock().await;
|
||||
state.subscription_state_by_id.insert(
|
||||
subscription_id,
|
||||
SubscriptionState {
|
||||
thread_id,
|
||||
connection_id,
|
||||
},
|
||||
);
|
||||
state
|
||||
.thread_ids_by_connection
|
||||
.entry(connection_id)
|
||||
.or_default()
|
||||
.insert(thread_id);
|
||||
let thread_entry = state.threads.entry(thread_id).or_default();
|
||||
thread_entry.connection_ids.insert(connection_id);
|
||||
thread_entry.state.clone()
|
||||
};
|
||||
self.subscription_state_by_id.insert(
|
||||
subscription_id,
|
||||
SubscriptionState {
|
||||
thread_id,
|
||||
connection_id,
|
||||
},
|
||||
);
|
||||
self.thread_ids_by_connection
|
||||
.entry(connection_id)
|
||||
.or_default()
|
||||
.insert(thread_id);
|
||||
let thread_state = self.thread_state(thread_id);
|
||||
{
|
||||
let mut thread_state_guard = thread_state.lock().await;
|
||||
thread_state_guard.add_connection(connection_id);
|
||||
thread_state_guard.set_experimental_raw_events(experimental_raw_events);
|
||||
}
|
||||
thread_state
|
||||
}
|
||||
|
||||
pub(crate) async fn try_ensure_connection_subscribed(
|
||||
&self,
|
||||
pub(crate) async fn ensure_connection_subscribed(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
connection_id: ConnectionId,
|
||||
experimental_raw_events: bool,
|
||||
) -> Option<Arc<Mutex<ThreadState>>> {
|
||||
let thread_state = {
|
||||
let mut state = self.state.lock().await;
|
||||
if !state.live_connections.contains(&connection_id) {
|
||||
return None;
|
||||
}
|
||||
state
|
||||
.thread_ids_by_connection
|
||||
.entry(connection_id)
|
||||
.or_default()
|
||||
.insert(thread_id);
|
||||
let thread_entry = state.threads.entry(thread_id).or_default();
|
||||
thread_entry.connection_ids.insert(connection_id);
|
||||
thread_entry.state.clone()
|
||||
};
|
||||
) -> Arc<Mutex<ThreadState>> {
|
||||
self.thread_ids_by_connection
|
||||
.entry(connection_id)
|
||||
.or_default()
|
||||
.insert(thread_id);
|
||||
let thread_state = self.thread_state(thread_id);
|
||||
{
|
||||
let mut thread_state_guard = thread_state.lock().await;
|
||||
thread_state_guard.add_connection(connection_id);
|
||||
if experimental_raw_events {
|
||||
thread_state_guard.set_experimental_raw_events(true);
|
||||
}
|
||||
}
|
||||
Some(thread_state)
|
||||
thread_state
|
||||
}
|
||||
|
||||
pub(crate) async fn try_add_connection_to_thread(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
connection_id: ConnectionId,
|
||||
) -> bool {
|
||||
let mut state = self.state.lock().await;
|
||||
if !state.live_connections.contains(&connection_id) {
|
||||
return false;
|
||||
}
|
||||
state
|
||||
pub(crate) async fn remove_connection(&mut self, connection_id: ConnectionId) {
|
||||
let thread_ids = self
|
||||
.thread_ids_by_connection
|
||||
.entry(connection_id)
|
||||
.or_default()
|
||||
.insert(thread_id);
|
||||
state
|
||||
.threads
|
||||
.entry(thread_id)
|
||||
.or_default()
|
||||
.connection_ids
|
||||
.insert(connection_id);
|
||||
true
|
||||
}
|
||||
.remove(&connection_id)
|
||||
.unwrap_or_default();
|
||||
self.subscription_state_by_id
|
||||
.retain(|_, state| state.connection_id != connection_id);
|
||||
|
||||
pub(crate) async fn remove_connection(&self, connection_id: ConnectionId) {
|
||||
let thread_states = {
|
||||
let mut state = self.state.lock().await;
|
||||
state.live_connections.remove(&connection_id);
|
||||
let thread_ids = state
|
||||
.thread_ids_by_connection
|
||||
.remove(&connection_id)
|
||||
.unwrap_or_default();
|
||||
state
|
||||
.subscription_state_by_id
|
||||
.retain(|_, state| state.connection_id != connection_id);
|
||||
for thread_id in &thread_ids {
|
||||
if let Some(thread_entry) = state.threads.get_mut(thread_id) {
|
||||
thread_entry.connection_ids.remove(&connection_id);
|
||||
if thread_ids.is_empty() {
|
||||
for thread_state in self.thread_states.values() {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.remove_connection(connection_id);
|
||||
if thread_state.subscribed_connection_ids().is_empty() {
|
||||
tracing::debug!(
|
||||
connection_id = ?connection_id,
|
||||
listener_generation = thread_state.listener_generation,
|
||||
"retaining thread listener after connection disconnect left zero subscribers"
|
||||
);
|
||||
}
|
||||
}
|
||||
thread_ids
|
||||
.into_iter()
|
||||
.map(|thread_id| {
|
||||
(
|
||||
thread_id,
|
||||
state
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.is_none_or(|thread_entry| thread_entry.connection_ids.is_empty()),
|
||||
state
|
||||
.threads
|
||||
.get(&thread_id)
|
||||
.map(|thread_entry| thread_entry.state.clone()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
for (thread_id, no_subscribers, thread_state) in thread_states {
|
||||
if !no_subscribers {
|
||||
continue;
|
||||
for thread_id in thread_ids {
|
||||
if let Some(thread_state) = self.thread_states.get(&thread_id) {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.remove_connection(connection_id);
|
||||
if thread_state.subscribed_connection_ids().is_empty() {
|
||||
tracing::debug!(
|
||||
thread_id = %thread_id,
|
||||
connection_id = ?connection_id,
|
||||
listener_generation = thread_state.listener_generation,
|
||||
"retaining thread listener after connection disconnect left zero subscribers"
|
||||
);
|
||||
}
|
||||
}
|
||||
let Some(thread_state) = thread_state else {
|
||||
continue;
|
||||
};
|
||||
let listener_generation = thread_state.lock().await.listener_generation;
|
||||
tracing::debug!(
|
||||
thread_id = %thread_id,
|
||||
connection_id = ?connection_id,
|
||||
listener_generation,
|
||||
"retaining thread listener after connection disconnect left zero subscribers"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +91,7 @@ impl ThreadWatchManager {
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_thread(&self, thread: Thread) {
|
||||
self.mutate_and_publish(move |state| state.upsert_thread(thread.id, true))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_thread_silently(&self, thread: Thread) {
|
||||
self.mutate_and_publish(move |state| state.upsert_thread(thread.id, false))
|
||||
self.mutate_and_publish(move |state| state.upsert_thread(thread.id))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -294,22 +289,14 @@ struct ThreadWatchState {
|
||||
}
|
||||
|
||||
impl ThreadWatchState {
|
||||
fn upsert_thread(
|
||||
&mut self,
|
||||
thread_id: String,
|
||||
emit_notification: bool,
|
||||
) -> Option<ThreadStatusChangedNotification> {
|
||||
fn upsert_thread(&mut self, thread_id: String) -> Option<ThreadStatusChangedNotification> {
|
||||
let previous_status = self.status_for(&thread_id);
|
||||
let runtime = self
|
||||
.runtime_by_thread_id
|
||||
.entry(thread_id.clone())
|
||||
.or_default();
|
||||
runtime.is_loaded = true;
|
||||
if emit_notification {
|
||||
self.status_changed_notification(thread_id, previous_status)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.status_changed_notification(thread_id, previous_status)
|
||||
}
|
||||
|
||||
fn remove_thread(&mut self, thread_id: &str) -> Option<ThreadStatusChangedNotification> {
|
||||
@@ -705,45 +692,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn silent_upsert_skips_initial_notification() {
|
||||
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(8);
|
||||
let manager = ThreadWatchManager::new_with_outgoing(Arc::new(OutgoingMessageSender::new(
|
||||
outgoing_tx,
|
||||
)));
|
||||
|
||||
manager
|
||||
.upsert_thread_silently(test_thread(
|
||||
INTERACTIVE_THREAD_ID,
|
||||
codex_app_server_protocol::SessionSource::Cli,
|
||||
))
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
manager
|
||||
.loaded_status_for_thread(INTERACTIVE_THREAD_ID)
|
||||
.await,
|
||||
ThreadStatus::Idle,
|
||||
);
|
||||
assert!(
|
||||
timeout(Duration::from_millis(100), outgoing_rx.recv())
|
||||
.await
|
||||
.is_err(),
|
||||
"silent upsert should not emit thread/status/changed"
|
||||
);
|
||||
|
||||
manager.note_turn_started(INTERACTIVE_THREAD_ID).await;
|
||||
assert_eq!(
|
||||
recv_status_changed_notification(&mut outgoing_rx).await,
|
||||
ThreadStatusChangedNotification {
|
||||
thread_id: INTERACTIVE_THREAD_ID.to_string(),
|
||||
status: ThreadStatus::Active {
|
||||
active_flags: vec![],
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn wait_for_status(
|
||||
manager: &ThreadWatchManager,
|
||||
thread_id: &str,
|
||||
@@ -785,7 +733,6 @@ mod tests {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
preview: String::new(),
|
||||
ephemeral: false,
|
||||
model_provider: "mock-provider".to_string(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
|
||||
@@ -744,7 +744,6 @@ mod tests {
|
||||
id: codex_app_server_protocol::RequestId::Integer(7),
|
||||
method: "config/read".to_string(),
|
||||
params: Some(json!({ "includeLayers": false })),
|
||||
trace: None,
|
||||
});
|
||||
assert!(
|
||||
enqueue_incoming_message(&transport_event_tx, &writer_tx, connection_id, request).await
|
||||
@@ -886,7 +885,6 @@ mod tests {
|
||||
id: codex_app_server_protocol::RequestId::Integer(7),
|
||||
method: "config/read".to_string(),
|
||||
params: Some(json!({ "includeLayers": false })),
|
||||
trace: None,
|
||||
});
|
||||
|
||||
let enqueue_result = tokio::time::timeout(
|
||||
|
||||
@@ -891,7 +891,6 @@ impl McpProcess {
|
||||
id: RequestId::Integer(request_id),
|
||||
method: method.to_string(),
|
||||
params,
|
||||
trace: None,
|
||||
});
|
||||
self.send_jsonrpc_message(message).await?;
|
||||
Ok(request_id)
|
||||
|
||||
@@ -84,7 +84,6 @@ pub fn create_fake_rollout_with_source(
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
let payload = serde_json::to_value(SessionMetaLine {
|
||||
meta,
|
||||
@@ -166,7 +165,6 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
};
|
||||
let payload = serde_json::to_value(SessionMetaLine {
|
||||
meta,
|
||||
|
||||
@@ -36,7 +36,7 @@ use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn test_codex_jsonrpc_conversation_flow() -> Result<()> {
|
||||
@@ -337,7 +337,6 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
|
||||
model: "mock-model".to_string(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -454,7 +453,6 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<(
|
||||
model: model.clone(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -483,7 +481,6 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<(
|
||||
model: model.clone(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -92,7 +92,6 @@ async fn send_user_turn_accepts_output_schema_v1() -> Result<()> {
|
||||
model: "mock-model".to_string(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: Some(output_schema.clone()),
|
||||
})
|
||||
.await?;
|
||||
@@ -185,7 +184,6 @@ async fn send_user_turn_rejects_oversized_input_v1() -> Result<()> {
|
||||
model: "mock-model".to_string(),
|
||||
effort: Some(ReasoningEffort::Low),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: None,
|
||||
})
|
||||
.await?;
|
||||
@@ -275,7 +273,6 @@ async fn send_user_turn_output_schema_is_per_turn_v1() -> Result<()> {
|
||||
model: "mock-model".to_string(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: Some(output_schema.clone()),
|
||||
})
|
||||
.await?;
|
||||
@@ -324,7 +321,6 @@ async fn send_user_turn_output_schema_is_per_turn_v1() -> Result<()> {
|
||||
model: "mock-model".to_string(),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: ReasoningSummary::Auto,
|
||||
service_tier: None,
|
||||
output_schema: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -628,7 +628,6 @@ fn append_rollout_turn_context(path: &Path, timestamp: &str, model: &str) -> std
|
||||
model: model.to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
realtime_active: Some(false),
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
user_instructions: None,
|
||||
|
||||
@@ -131,7 +131,6 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
|
||||
payload.auth_mode.is_none(),
|
||||
"auth_method should be None after logout"
|
||||
);
|
||||
assert_eq!(payload.plan_type, None);
|
||||
|
||||
assert!(
|
||||
!codex_home.path().join("auth.json").exists(),
|
||||
@@ -202,7 +201,6 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
assert_eq!(payload.auth_mode, Some(AuthMode::ChatgptAuthTokens));
|
||||
assert_eq!(payload.plan_type, Some(AccountPlanType::Pro));
|
||||
|
||||
let get_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
@@ -845,7 +843,6 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
|
||||
bail!("unexpected notification: {parsed:?}");
|
||||
};
|
||||
pretty_assertions::assert_eq!(payload.auth_mode, Some(AuthMode::ApiKey));
|
||||
pretty_assertions::assert_eq!(payload.plan_type, None);
|
||||
|
||||
assert!(codex_home.path().join("auth.json").exists());
|
||||
Ok(())
|
||||
@@ -1230,45 +1227,3 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(
|
||||
codex_home.path(),
|
||||
CreateConfigTomlParams {
|
||||
requires_openai_auth: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt").email("user@example.com"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
refresh_token: false,
|
||||
};
|
||||
let request_id = mcp.send_get_account_request(params).await?;
|
||||
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: GetAccountResponse = to_response(resp)?;
|
||||
|
||||
let expected = GetAccountResponse {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: "user@example.com".to_string(),
|
||||
plan_type: AccountPlanType::Unknown,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
assert_eq!(received, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn app_server_default_analytics_disabled_without_flag() -> Result<()> {
|
||||
let provider = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
SERVICE_VERSION,
|
||||
Some("codex-app-server"),
|
||||
Some("codex_app_server"),
|
||||
false,
|
||||
)
|
||||
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
|
||||
@@ -55,7 +55,7 @@ async fn app_server_default_analytics_enabled_with_flag() -> Result<()> {
|
||||
let provider = codex_core::otel_init::build_provider(
|
||||
&config,
|
||||
SERVICE_VERSION,
|
||||
Some("codex-app-server"),
|
||||
Some("codex_app_server"),
|
||||
true,
|
||||
)
|
||||
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
|
||||
|
||||
@@ -428,7 +428,7 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates() -> Result<()> {
|
||||
async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> {
|
||||
let connectors = vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
@@ -475,7 +475,7 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-directory-first")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
@@ -492,14 +492,60 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let maybe_update = timeout(
|
||||
Duration::from_millis(150),
|
||||
read_app_list_updated_notification(&mut mcp),
|
||||
)
|
||||
.await;
|
||||
let expected_directory_first = vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: Some("https://example.com/alpha.png".to_string()),
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "beta".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
];
|
||||
let expected_accessible_first = vec![AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta App".to_string(),
|
||||
description: None,
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: true,
|
||||
is_enabled: true,
|
||||
}];
|
||||
|
||||
let first_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
// app/list emits an update after whichever async load finishes first. Even with
|
||||
// a tools delay in this test, the accessible-tools path can return first if the
|
||||
// process-global Codex Apps tools cache is warm from another test.
|
||||
assert!(
|
||||
maybe_update.is_err(),
|
||||
"unexpected directory-only app/list update before accessible apps loaded"
|
||||
first_update.data == expected_directory_first
|
||||
|| first_update.data == expected_accessible_first,
|
||||
"unexpected first app/list update: {:#?}",
|
||||
first_update.data
|
||||
);
|
||||
|
||||
let expected = vec![
|
||||
@@ -533,96 +579,8 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
},
|
||||
];
|
||||
|
||||
let update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(update.data, expected);
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let AppsListResponse { data, next_cursor } = to_response(response)?;
|
||||
assert_eq!(data, expected);
|
||||
assert!(next_cursor.is_none());
|
||||
|
||||
server_handle.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
|
||||
let connectors = vec![AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
}];
|
||||
let (server_url, server_handle) = start_apps_server_with_delays(
|
||||
connectors.clone(),
|
||||
Vec::new(),
|
||||
Duration::from_millis(300),
|
||||
Duration::ZERO,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_connectors_config(codex_home.path(), &server_url)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-empty-interim")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_apps_list_request(AppsListParams {
|
||||
limit: None,
|
||||
cursor: None,
|
||||
thread_id: None,
|
||||
force_refetch: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let maybe_update = timeout(
|
||||
Duration::from_millis(150),
|
||||
read_app_list_updated_notification(&mut mcp),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
maybe_update.is_err(),
|
||||
"unexpected empty interim app/list update"
|
||||
);
|
||||
|
||||
let expected = vec![AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
}];
|
||||
|
||||
let update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(update.data, expected);
|
||||
let second_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(second_update.data, expected);
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
@@ -1068,14 +1026,39 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
]
|
||||
);
|
||||
|
||||
let maybe_second_update = timeout(
|
||||
Duration::from_millis(150),
|
||||
read_app_list_updated_notification(&mut mcp),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
maybe_second_update.is_err(),
|
||||
"unexpected inaccessible-only app/list update during force refetch"
|
||||
let second_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(
|
||||
second_update.data,
|
||||
vec![
|
||||
AppInfo {
|
||||
id: "alpha".to_string(),
|
||||
name: "Alpha".to_string(),
|
||||
description: Some("Alpha v1".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta App".to_string(),
|
||||
description: Some("Beta v1".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()),
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let expected_final = vec![AppInfo {
|
||||
@@ -1092,8 +1075,8 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
}];
|
||||
let second_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(second_update.data, expected_final);
|
||||
let third_update = read_app_list_updated_notification(&mut mcp).await?;
|
||||
assert_eq!(third_update.data, expected_final);
|
||||
|
||||
let refetch_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
|
||||
@@ -174,7 +174,6 @@ pub(super) async fn send_request(
|
||||
id: RequestId::Integer(id),
|
||||
method: method.to_string(),
|
||||
params,
|
||||
trace: None,
|
||||
});
|
||||
send_jsonrpc(stream, message).await
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginApiKeyParams;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadRealtimeAppendAudioParams;
|
||||
use codex_app_server_protocol::ThreadRealtimeAppendAudioResponse;
|
||||
@@ -44,16 +43,19 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let realtime_server = start_websocket_server(vec![vec![
|
||||
vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_backend", "instructions": "backend prompt" }
|
||||
"type": "session.created",
|
||||
"session": { "id": "sess_backend" }
|
||||
})],
|
||||
vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "backend_prompt": "backend prompt" }
|
||||
})],
|
||||
vec![],
|
||||
vec![
|
||||
json!({
|
||||
"type": "conversation.output_audio.delta",
|
||||
"type": "response.output_audio.delta",
|
||||
"delta": "AQID",
|
||||
"sample_rate": 24_000,
|
||||
"channels": 1,
|
||||
"num_channels": 1,
|
||||
"samples_per_channel": 512
|
||||
}),
|
||||
json!({
|
||||
@@ -82,7 +84,6 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
mcp.initialize().await?;
|
||||
login_with_api_key(&mut mcp, "sk-test-key").await?;
|
||||
|
||||
let thread_start_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
@@ -181,7 +182,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
assert_eq!(connection.len(), 3);
|
||||
assert_eq!(
|
||||
connection[0].body_json()["type"].as_str(),
|
||||
Some("session.update")
|
||||
Some("session.create")
|
||||
);
|
||||
let mut request_types = [
|
||||
connection[1].body_json()["type"]
|
||||
@@ -198,7 +199,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
|
||||
request_types,
|
||||
[
|
||||
"conversation.item.create".to_string(),
|
||||
"input_audio_buffer.append".to_string(),
|
||||
"response.input_audio.delta".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -213,8 +214,8 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
let responses_server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let realtime_server = start_websocket_server(vec![vec![
|
||||
vec![json!({
|
||||
"type": "session.updated",
|
||||
"session": { "id": "sess_backend", "instructions": "backend prompt" }
|
||||
"type": "session.created",
|
||||
"session": { "id": "sess_backend" }
|
||||
})],
|
||||
vec![],
|
||||
]])
|
||||
@@ -230,7 +231,6 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
mcp.initialize().await?;
|
||||
login_with_api_key(&mut mcp, "sk-test-key").await?;
|
||||
|
||||
let thread_start_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
@@ -349,22 +349,6 @@ async fn read_notification<T: DeserializeOwned>(mcp: &mut McpProcess, method: &s
|
||||
Ok(serde_json::from_value(params)?)
|
||||
}
|
||||
|
||||
async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> {
|
||||
let request_id = mcp
|
||||
.send_login_api_key_request(LoginApiKeyParams {
|
||||
api_key: api_key.to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(
|
||||
codex_home: &Path,
|
||||
responses_server_uri: &str,
|
||||
|
||||
@@ -4,11 +4,9 @@ use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_request_user_input_sse_response;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerRequestResolvedNotification;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -88,7 +86,6 @@ async fn request_user_input_round_trip() -> Result<()> {
|
||||
assert_eq!(params.turn_id, turn.id);
|
||||
assert_eq!(params.item_id, "call1");
|
||||
assert_eq!(params.questions.len(), 1);
|
||||
let resolved_request_id = request_id.clone();
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
@@ -99,31 +96,17 @@ async fn request_user_input_round_trip() -> Result<()> {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let mut saw_resolved = false;
|
||||
loop {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
match notification.method.as_str() {
|
||||
"serverRequest/resolved" => {
|
||||
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("serverRequest/resolved params"),
|
||||
)?;
|
||||
assert_eq!(resolved.thread_id, thread.id);
|
||||
assert_eq!(resolved.request_id, resolved_request_id);
|
||||
saw_resolved = true;
|
||||
}
|
||||
"turn/completed" => {
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -20,12 +19,9 @@ use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
@@ -305,31 +301,6 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<(
|
||||
"detached review should run on a different thread"
|
||||
);
|
||||
|
||||
let deadline = tokio::time::Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
let notification = loop {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
let message = timeout(remaining, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
if notification.method == "thread/status/changed" {
|
||||
let status_changed: ThreadStatusChangedNotification =
|
||||
serde_json::from_value(notification.params.expect("params must be present"))?;
|
||||
if status_changed.thread_id == review_thread_id {
|
||||
anyhow::bail!(
|
||||
"detached review threads should be introduced without a preceding thread/status/changed"
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if notification.method == "thread/started" {
|
||||
break notification;
|
||||
}
|
||||
};
|
||||
let started: ThreadStartedNotification =
|
||||
serde_json::from_value(notification.params.expect("params must be present"))?;
|
||||
assert_eq!(started.thread.id, review_thread_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -418,11 +389,6 @@ async fn start_default_thread(mcp: &mut McpProcess) -> Result<String> {
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/started"),
|
||||
)
|
||||
.await??;
|
||||
Ok(thread.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use app_test_support::create_fake_rollout;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SessionSource;
|
||||
@@ -15,7 +15,6 @@ use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -125,27 +124,11 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
}
|
||||
|
||||
// A corresponding thread/started notification should arrive.
|
||||
let deadline = tokio::time::Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
let notif = loop {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
let message = timeout(remaining, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notif) = message else {
|
||||
continue;
|
||||
};
|
||||
if notif.method == "thread/status/changed" {
|
||||
let status_changed: ThreadStatusChangedNotification =
|
||||
serde_json::from_value(notif.params.expect("params must be present"))?;
|
||||
if status_changed.thread_id == thread.id {
|
||||
anyhow::bail!(
|
||||
"thread/fork should introduce the thread without a preceding thread/status/changed"
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if notif.method == "thread/started" {
|
||||
break notif;
|
||||
}
|
||||
};
|
||||
let notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/started"),
|
||||
)
|
||||
.await??;
|
||||
let started_params = notif.params.clone().expect("params must be present");
|
||||
let started_thread_json = started_params
|
||||
.get("thread")
|
||||
|
||||
@@ -10,7 +10,6 @@ use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadNameUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
@@ -79,7 +78,6 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> {
|
||||
assert_eq!(thread.id, conversation_id);
|
||||
assert_eq!(thread.preview, preview);
|
||||
assert_eq!(thread.model_provider, "mock_provider");
|
||||
assert!(!thread.ephemeral, "stored rollouts should not be ephemeral");
|
||||
assert!(thread.path.as_ref().expect("thread path").is_absolute());
|
||||
assert_eq!(thread.cwd, PathBuf::from("/"));
|
||||
assert_eq!(thread.cli_version, "0.0.0");
|
||||
@@ -221,6 +219,25 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// `thread/name/set` operates on loaded threads (via ThreadManager). A rollout existing on disk
|
||||
// is not enough; we must `thread/resume` first to load it into the running server.
|
||||
let pre_resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: conversation_id.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let pre_resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(pre_resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse {
|
||||
thread: pre_resumed,
|
||||
..
|
||||
} = to_response::<ThreadResumeResponse>(pre_resume_resp)?;
|
||||
assert_eq!(pre_resumed.id, conversation_id);
|
||||
|
||||
// Set a user-facing thread title.
|
||||
let new_name = "My renamed thread";
|
||||
let set_id = mcp
|
||||
@@ -235,15 +252,6 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(set_resp)?;
|
||||
let notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/name/updated"),
|
||||
)
|
||||
.await??;
|
||||
let notification: ThreadNameUpdatedNotification =
|
||||
serde_json::from_value(notification.params.expect("thread/name/updated params"))?;
|
||||
assert_eq!(notification.thread_id, conversation_id);
|
||||
assert_eq!(notification.thread_name.as_deref(), Some(new_name));
|
||||
|
||||
// Read should now surface `thread.name`, and the wire payload must include `name`.
|
||||
let read_id = mcp
|
||||
@@ -270,11 +278,6 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
Some(new_name),
|
||||
"thread/read must serialize `thread.name` on the wire"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"thread/read must serialize `thread.ephemeral` on the wire"
|
||||
);
|
||||
|
||||
// List should also surface the name.
|
||||
let list_id = mcp
|
||||
@@ -314,11 +317,6 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
Some(new_name),
|
||||
"thread/list must serialize `thread.name` on the wire"
|
||||
);
|
||||
assert_eq!(
|
||||
listed_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"thread/list must serialize `thread.ephemeral` on the wire"
|
||||
);
|
||||
|
||||
// Resume should also surface the name.
|
||||
let resume_id = mcp
|
||||
@@ -347,11 +345,6 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
|
||||
Some(new_name),
|
||||
"thread/resume must serialize `thread.name` on the wire"
|
||||
);
|
||||
assert_eq!(
|
||||
resumed_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"thread/resume must serialize `thread.ephemeral` on the wire"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_apply_patch_sse_response;
|
||||
use app_test_support::create_fake_rollout_with_text_elements;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::rollout_path;
|
||||
use app_test_support::to_response;
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::FileChangeApprovalDecision;
|
||||
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
@@ -652,306 +639,6 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_replays_pending_command_execution_request_approval() -> Result<()> {
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("seeded")?,
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
None,
|
||||
Some(5000),
|
||||
"call-1",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut primary = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
|
||||
|
||||
let start_id = primary
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("gpt-5.1-codex-max".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let seed_turn_id = primary
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "seed history".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
primary.clear_message_buffer();
|
||||
|
||||
let running_turn_id = primary
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "run command".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
approval_policy: Some(AskForApproval::UnlessTrusted),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let original_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { .. } = &original_request else {
|
||||
panic!("expected CommandExecutionRequestApproval request, got {original_request:?}");
|
||||
};
|
||||
|
||||
let resume_id = primary
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse {
|
||||
thread: resumed_thread,
|
||||
..
|
||||
} = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(resumed_thread.id, thread.id);
|
||||
assert!(
|
||||
resumed_thread
|
||||
.turns
|
||||
.iter()
|
||||
.any(|turn| matches!(turn.status, TurnStatus::InProgress))
|
||||
);
|
||||
|
||||
let replayed_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
pretty_assertions::assert_eq!(replayed_request, original_request);
|
||||
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = replayed_request else {
|
||||
panic!("expected CommandExecutionRequestApproval request");
|
||||
};
|
||||
primary
|
||||
.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Accept,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_replays_pending_file_change_request_approval() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
|
||||
let patch = r#"*** Begin Patch
|
||||
*** Add File: README.md
|
||||
+new line
|
||||
*** End Patch
|
||||
"#;
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("seeded")?,
|
||||
create_apply_patch_sse_response(patch, "patch-call")?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
create_config_toml(&codex_home, &server.uri())?;
|
||||
|
||||
let mut primary = McpProcess::new(&codex_home).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
|
||||
|
||||
let start_id = primary
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("gpt-5.1-codex-max".to_string()),
|
||||
cwd: Some(workspace.to_string_lossy().into_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let seed_turn_id = primary
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "seed history".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(workspace.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
primary.clear_message_buffer();
|
||||
|
||||
let running_turn_id = primary
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "apply patch".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(workspace.clone()),
|
||||
approval_policy: Some(AskForApproval::UnlessTrusted),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let original_started = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let notification = primary
|
||||
.read_stream_until_notification_message("item/started")
|
||||
.await?;
|
||||
let started: ItemStartedNotification =
|
||||
serde_json::from_value(notification.params.clone().expect("item/started params"))?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let expected_readme_path = workspace.join("README.md");
|
||||
let expected_file_change = ThreadItem::FileChange {
|
||||
id: "patch-call".to_string(),
|
||||
changes: vec![codex_app_server_protocol::FileUpdateChange {
|
||||
path: expected_readme_path.to_string_lossy().into_owned(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "new line\n".to_string(),
|
||||
}],
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
assert_eq!(original_started, expected_file_change);
|
||||
|
||||
let original_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::FileChangeRequestApproval { .. } = &original_request else {
|
||||
panic!("expected FileChangeRequestApproval request, got {original_request:?}");
|
||||
};
|
||||
primary.clear_message_buffer();
|
||||
|
||||
let resume_id = primary
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse {
|
||||
thread: resumed_thread,
|
||||
..
|
||||
} = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
assert_eq!(resumed_thread.id, thread.id);
|
||||
assert!(
|
||||
resumed_thread
|
||||
.turns
|
||||
.iter()
|
||||
.any(|turn| matches!(turn.status, TurnStatus::InProgress))
|
||||
);
|
||||
|
||||
let replayed_request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(replayed_request, original_request);
|
||||
|
||||
let ServerRequest::FileChangeRequestApproval { request_id, .. } = replayed_request else {
|
||||
panic!("expected FileChangeRequestApproval request");
|
||||
};
|
||||
primary
|
||||
.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(FileChangeRequestApprovalResponse {
|
||||
decision: FileChangeApprovalDecision::Accept,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
primary.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -3,18 +3,16 @@ use app_test_support::McpProcess;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::ThreadStatusChangedNotification;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
@@ -64,10 +62,6 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
thread.created_at > 0,
|
||||
"created_at should be a positive UNIX timestamp"
|
||||
);
|
||||
assert!(
|
||||
!thread.ephemeral,
|
||||
"new persistent threads should not be ephemeral"
|
||||
);
|
||||
assert_eq!(thread.status, ThreadStatus::Idle);
|
||||
let thread_path = thread.path.clone().expect("thread path should be present");
|
||||
assert!(thread_path.is_absolute(), "thread path should be absolute");
|
||||
@@ -86,35 +80,14 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
Some(&Value::Null),
|
||||
"new threads should serialize `name: null`"
|
||||
);
|
||||
assert_eq!(
|
||||
thread_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"new persistent threads should serialize `ephemeral: false`"
|
||||
);
|
||||
assert_eq!(thread.name, None);
|
||||
|
||||
// A corresponding thread/started notification should arrive.
|
||||
let deadline = tokio::time::Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
let notif = loop {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
let message = timeout(remaining, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notif) = message else {
|
||||
continue;
|
||||
};
|
||||
if notif.method == "thread/status/changed" {
|
||||
let status_changed: ThreadStatusChangedNotification =
|
||||
serde_json::from_value(notif.params.expect("params must be present"))?;
|
||||
if status_changed.thread_id == thread.id {
|
||||
anyhow::bail!(
|
||||
"thread/start should introduce the thread without a preceding thread/status/changed"
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if notif.method == "thread/started" {
|
||||
break notif;
|
||||
}
|
||||
};
|
||||
let notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("thread/started"),
|
||||
)
|
||||
.await??;
|
||||
let started_params = notif.params.clone().expect("params must be present");
|
||||
let started_thread_json = started_params
|
||||
.get("thread")
|
||||
@@ -125,13 +98,6 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
Some(&Value::Null),
|
||||
"thread/started should serialize `name: null` for new threads"
|
||||
);
|
||||
assert_eq!(
|
||||
started_thread_json
|
||||
.get("ephemeral")
|
||||
.and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"thread/started should serialize `ephemeral: false` for new persistent threads"
|
||||
);
|
||||
let started: ThreadStartedNotification =
|
||||
serde_json::from_value(notif.params.expect("params must be present"))?;
|
||||
assert_eq!(started.thread, thread);
|
||||
@@ -230,25 +196,11 @@ async fn thread_start_ephemeral_remains_pathless() -> Result<()> {
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
|
||||
)
|
||||
.await??;
|
||||
let resp_result = resp.result.clone();
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(resp)?;
|
||||
assert!(
|
||||
thread.ephemeral,
|
||||
"ephemeral threads should be marked explicitly"
|
||||
);
|
||||
assert_eq!(
|
||||
thread.path, None,
|
||||
"ephemeral threads should not expose a path"
|
||||
);
|
||||
let thread_json = resp_result
|
||||
.get("thread")
|
||||
.and_then(Value::as_object)
|
||||
.expect("thread/start result.thread must be an object");
|
||||
assert_eq!(
|
||||
thread_json.get("ephemeral").and_then(Value::as_bool),
|
||||
Some(true),
|
||||
"ephemeral threads should serialize `ephemeral: true`"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerRequestResolvedNotification;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
@@ -50,7 +48,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> {
|
||||
"call_sleep",
|
||||
)?])
|
||||
.await;
|
||||
create_config_toml(&codex_home, &server.uri(), "never")?;
|
||||
create_config_toml(&codex_home, &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(&codex_home).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
@@ -122,134 +120,15 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_interrupt_resolves_pending_command_approval_request() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let shell_command = vec![
|
||||
"powershell".to_string(),
|
||||
"-Command".to_string(),
|
||||
"Start-Sleep -Seconds 10".to_string(),
|
||||
];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let shell_command = vec!["sleep".to_string(), "10".to_string()];
|
||||
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let working_directory = tmp.path().join("workdir");
|
||||
std::fs::create_dir(&working_directory)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence(vec![create_shell_command_sse_response(
|
||||
shell_command.clone(),
|
||||
Some(&working_directory),
|
||||
Some(10_000),
|
||||
"call_sleep_approval",
|
||||
)?])
|
||||
.await;
|
||||
create_config_toml(&codex_home, &server.uri(), "untrusted")?;
|
||||
|
||||
let mut mcp = McpProcess::new(&codex_home).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
|
||||
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "run sleep".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(working_directory),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
let request = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, params } = request else {
|
||||
panic!("expected CommandExecutionRequestApproval request");
|
||||
};
|
||||
assert_eq!(params.item_id, "call_sleep_approval");
|
||||
assert_eq!(params.thread_id, thread.id);
|
||||
assert_eq!(params.turn_id, turn.id);
|
||||
|
||||
let interrupt_id = mcp
|
||||
.send_turn_interrupt_request(TurnInterruptParams {
|
||||
thread_id: thread.id.clone(),
|
||||
turn_id: turn.id.clone(),
|
||||
})
|
||||
.await?;
|
||||
let interrupt_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(interrupt_id)),
|
||||
)
|
||||
.await??;
|
||||
let _resp: TurnInterruptResponse = to_response::<TurnInterruptResponse>(interrupt_resp)?;
|
||||
|
||||
let resolved_notification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("serverRequest/resolved"),
|
||||
)
|
||||
.await??;
|
||||
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
|
||||
resolved_notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("serverRequest/resolved params must be present"),
|
||||
)?;
|
||||
assert_eq!(resolved.thread_id, thread.id);
|
||||
assert_eq!(resolved.request_id, request_id);
|
||||
|
||||
let completed_notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let completed: TurnCompletedNotification = serde_json::from_value(
|
||||
completed_notif
|
||||
.params
|
||||
.expect("turn/completed params must be present"),
|
||||
)?;
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Interrupted);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper to create a config.toml pointing at the mock model server.
|
||||
fn create_config_toml(
|
||||
codex_home: &std::path::Path,
|
||||
server_uri: &str,
|
||||
approval_policy: &str,
|
||||
) -> std::io::Result<()> {
|
||||
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "{approval_policy}"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
@@ -22,14 +22,12 @@ use codex_app_server_protocol::FileChangeRequestApprovalResponse;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerRequestResolvedNotification;
|
||||
use codex_app_server_protocol::TextElement;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -434,21 +432,6 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
started.turn.status,
|
||||
codex_app_server_protocol::TurnStatus::InProgress
|
||||
);
|
||||
assert_eq!(started.turn.id, turn.id);
|
||||
|
||||
let completed_notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let completed: TurnCompletedNotification = serde_json::from_value(
|
||||
completed_notif
|
||||
.params
|
||||
.expect("turn/completed params must be present"),
|
||||
)?;
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.id, turn.id);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
|
||||
// Send a second turn that exercises the overrides path: change the model.
|
||||
let turn_req2 = mcp
|
||||
@@ -472,30 +455,25 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<(
|
||||
// Ensure the second turn has a different id than the first.
|
||||
assert_ne!(turn.id, turn2.id);
|
||||
|
||||
let notif2: JSONRPCNotification = timeout(
|
||||
// Expect a second turn/started notification as well.
|
||||
let _notif2: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await??;
|
||||
let started2: TurnStartedNotification =
|
||||
serde_json::from_value(notif2.params.expect("params must be present"))?;
|
||||
assert_eq!(started2.thread_id, thread.id);
|
||||
assert_eq!(started2.turn.id, turn2.id);
|
||||
assert_eq!(started2.turn.status, TurnStatus::InProgress);
|
||||
|
||||
let completed_notif2: JSONRPCNotification = timeout(
|
||||
let completed_notif: JSONRPCNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let completed2: TurnCompletedNotification = serde_json::from_value(
|
||||
completed_notif2
|
||||
let completed: TurnCompletedNotification = serde_json::from_value(
|
||||
completed_notif
|
||||
.params
|
||||
.expect("turn/completed params must be present"),
|
||||
)?;
|
||||
assert_eq!(completed2.thread_id, thread.id);
|
||||
assert_eq!(completed2.turn.id, turn2.id);
|
||||
assert_eq!(completed2.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1093,7 +1071,6 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
|
||||
panic!("expected CommandExecutionRequestApproval request");
|
||||
};
|
||||
assert_eq!(params.item_id, "call1");
|
||||
let resolved_request_id = request_id.clone();
|
||||
|
||||
// Approve and wait for task completion
|
||||
mcp.send_response(
|
||||
@@ -1103,31 +1080,16 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let mut saw_resolved = false;
|
||||
loop {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
match notification.method.as_str() {
|
||||
"serverRequest/resolved" => {
|
||||
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("serverRequest/resolved params"),
|
||||
)?;
|
||||
assert_eq!(resolved.thread_id, thread.id);
|
||||
assert_eq!(resolved.request_id, resolved_request_id);
|
||||
saw_resolved = true;
|
||||
}
|
||||
"turn/completed" => {
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
// Second turn with approval_policy=never should not elicit approval
|
||||
let second_turn_id = mcp
|
||||
@@ -1385,7 +1347,6 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
model: Some("mock-model".to_string()),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: Some(ReasoningSummary::Auto),
|
||||
service_tier: None,
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
@@ -1417,7 +1378,6 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
|
||||
model: Some("mock-model".to_string()),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
summary: Some(ReasoningSummary::Auto),
|
||||
service_tier: None,
|
||||
personality: None,
|
||||
output_schema: None,
|
||||
collaboration_mode: None,
|
||||
@@ -1567,7 +1527,6 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
assert_eq!(params.item_id, "patch-call");
|
||||
assert_eq!(params.thread_id, thread.id);
|
||||
assert_eq!(params.turn_id, turn.id);
|
||||
let resolved_request_id = request_id.clone();
|
||||
let expected_readme_path = workspace.join("README.md");
|
||||
let expected_readme_path = expected_readme_path.to_string_lossy().into_owned();
|
||||
pretty_assertions::assert_eq!(
|
||||
@@ -1586,49 +1545,18 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let mut saw_resolved = false;
|
||||
let mut output_delta: Option<FileChangeOutputDeltaNotification> = None;
|
||||
let mut completed_file_change: Option<ThreadItem> = None;
|
||||
while !(output_delta.is_some() && completed_file_change.is_some()) {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
continue;
|
||||
};
|
||||
match notification.method.as_str() {
|
||||
"serverRequest/resolved" => {
|
||||
let resolved: ServerRequestResolvedNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("serverRequest/resolved params"),
|
||||
)?;
|
||||
assert_eq!(resolved.thread_id, thread.id);
|
||||
assert_eq!(resolved.request_id, resolved_request_id);
|
||||
saw_resolved = true;
|
||||
}
|
||||
"item/fileChange/outputDelta" => {
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
let notification: FileChangeOutputDeltaNotification = serde_json::from_value(
|
||||
notification
|
||||
.params
|
||||
.clone()
|
||||
.expect("item/fileChange/outputDelta params"),
|
||||
)?;
|
||||
output_delta = Some(notification);
|
||||
}
|
||||
"item/completed" => {
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
notification.params.clone().expect("item/completed params"),
|
||||
)?;
|
||||
if let ThreadItem::FileChange { .. } = completed.item {
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
completed_file_change = Some(completed.item);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let output_delta = output_delta.expect("file change output delta should be observed");
|
||||
|
||||
let output_delta_notif = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("item/fileChange/outputDelta"),
|
||||
)
|
||||
.await??;
|
||||
let output_delta: FileChangeOutputDeltaNotification = serde_json::from_value(
|
||||
output_delta_notif
|
||||
.params
|
||||
.clone()
|
||||
.expect("item/fileChange/outputDelta params"),
|
||||
)?;
|
||||
assert_eq!(output_delta.thread_id, thread.id);
|
||||
assert_eq!(output_delta.turn_id, turn.id);
|
||||
assert_eq!(output_delta.item_id, "patch-call");
|
||||
@@ -1638,23 +1566,38 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
output_delta.delta
|
||||
);
|
||||
|
||||
let completed_file_change =
|
||||
completed_file_change.expect("file change completion should be observed");
|
||||
let completed_file_change = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let completed_notif = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
completed_notif
|
||||
.params
|
||||
.clone()
|
||||
.expect("item/completed params"),
|
||||
)?;
|
||||
if let ThreadItem::FileChange { .. } = completed.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else {
|
||||
unreachable!("loop ensures we break on file change items");
|
||||
};
|
||||
assert_eq!(id, "patch-call");
|
||||
assert_eq!(status, PatchApplyStatus::Completed);
|
||||
|
||||
let readme_contents = std::fs::read_to_string(expected_readme_path)?;
|
||||
assert_eq!(readme_contents, "new line\n");
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let readme_contents = std::fs::read_to_string(expected_readme_path)?;
|
||||
assert_eq!(readme_contents, "new line\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "artifact-presentation",
|
||||
crate_name = "codex_artifact_presentation",
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
[package]
|
||||
name = "codex-artifact-presentation"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_artifact_presentation"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
image = { workspace = true, features = ["jpeg", "png"] }
|
||||
ppt-rs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tiny_http = { workspace = true }
|
||||
@@ -1,6 +0,0 @@
|
||||
mod presentation_artifact;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use presentation_artifact::*;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "artifact-spreadsheet",
|
||||
crate_name = "codex_artifact_spreadsheet",
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "codex-artifact-spreadsheet"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_artifact_spreadsheet"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
@@ -1,245 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::SpreadsheetArtifactError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct CellAddress {
|
||||
pub column: u32,
|
||||
pub row: u32,
|
||||
}
|
||||
|
||||
impl CellAddress {
|
||||
pub fn parse(address: &str) -> Result<Self, SpreadsheetArtifactError> {
|
||||
let trimmed = address.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "address is empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut split = 0usize;
|
||||
for (index, ch) in trimmed.char_indices() {
|
||||
if ch.is_ascii_alphabetic() {
|
||||
split = index + ch.len_utf8();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (letters, digits) = trimmed.split_at(split);
|
||||
if letters.is_empty() || digits.is_empty() {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "expected A1-style address".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !letters.chars().all(|ch| ch.is_ascii_alphabetic())
|
||||
|| !digits.chars().all(|ch| ch.is_ascii_digit())
|
||||
{
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "expected letters followed by digits".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let column = column_letters_to_index(letters)?;
|
||||
let row = digits
|
||||
.parse::<u32>()
|
||||
.map_err(|_| SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "row must be a positive integer".to_string(),
|
||||
})?;
|
||||
|
||||
if row == 0 {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "row must be positive".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self { column, row })
|
||||
}
|
||||
|
||||
pub fn to_a1(self) -> String {
|
||||
format!("{}{}", column_index_to_letters(self.column), self.row)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CellRange {
|
||||
pub start: CellAddress,
|
||||
pub end: CellAddress,
|
||||
}
|
||||
|
||||
impl CellRange {
|
||||
pub fn parse(address: &str) -> Result<Self, SpreadsheetArtifactError> {
|
||||
let trimmed = address.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: address.to_string(),
|
||||
message: "range is empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let (start, end) = if let Some((left, right)) = trimmed.split_once(':') {
|
||||
(CellAddress::parse(left)?, CellAddress::parse(right)?)
|
||||
} else {
|
||||
let cell = CellAddress::parse(trimmed)?;
|
||||
(cell, cell)
|
||||
};
|
||||
|
||||
let normalized = Self {
|
||||
start: CellAddress {
|
||||
column: start.column.min(end.column),
|
||||
row: start.row.min(end.row),
|
||||
},
|
||||
end: CellAddress {
|
||||
column: start.column.max(end.column),
|
||||
row: start.row.max(end.row),
|
||||
},
|
||||
};
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub fn from_start_end(start: CellAddress, end: CellAddress) -> Self {
|
||||
Self {
|
||||
start: CellAddress {
|
||||
column: start.column.min(end.column),
|
||||
row: start.row.min(end.row),
|
||||
},
|
||||
end: CellAddress {
|
||||
column: start.column.max(end.column),
|
||||
row: start.row.max(end.row),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_a1(&self) -> String {
|
||||
if self.is_single_cell() {
|
||||
self.start.to_a1()
|
||||
} else {
|
||||
format!("{}:{}", self.start.to_a1(), self.end.to_a1())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_single_cell(&self) -> bool {
|
||||
self.start == self.end
|
||||
}
|
||||
|
||||
pub fn is_single_row(&self) -> bool {
|
||||
self.start.row == self.end.row
|
||||
}
|
||||
|
||||
pub fn is_single_column(&self) -> bool {
|
||||
self.start.column == self.end.column
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
(self.end.column - self.start.column + 1) as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
(self.end.row - self.start.row + 1) as usize
|
||||
}
|
||||
|
||||
pub fn contains(&self, address: CellAddress) -> bool {
|
||||
self.start.column <= address.column
|
||||
&& address.column <= self.end.column
|
||||
&& self.start.row <= address.row
|
||||
&& address.row <= self.end.row
|
||||
}
|
||||
|
||||
pub fn contains_range(&self, other: &CellRange) -> bool {
|
||||
self.contains(other.start) && self.contains(other.end)
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: &CellRange) -> bool {
|
||||
!(self.end.column < other.start.column
|
||||
|| other.end.column < self.start.column
|
||||
|| self.end.row < other.start.row
|
||||
|| other.end.row < self.start.row)
|
||||
}
|
||||
|
||||
pub fn addresses(&self) -> impl Iterator<Item = CellAddress> {
|
||||
let range = self.clone();
|
||||
(range.start.row..=range.end.row).flat_map(move |row| {
|
||||
let range = range.clone();
|
||||
(range.start.column..=range.end.column).map(move |column| CellAddress { column, row })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column_letters_to_index(column: &str) -> Result<u32, SpreadsheetArtifactError> {
|
||||
let trimmed = column.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: column.to_string(),
|
||||
message: "column is empty".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut result = 0u32;
|
||||
for ch in trimmed.chars() {
|
||||
if !ch.is_ascii_alphabetic() {
|
||||
return Err(SpreadsheetArtifactError::InvalidAddress {
|
||||
address: column.to_string(),
|
||||
message: "column must contain only letters".to_string(),
|
||||
});
|
||||
}
|
||||
result = result
|
||||
.checked_mul(26)
|
||||
.and_then(|value| value.checked_add((ch.to_ascii_uppercase() as u8 - b'A' + 1) as u32))
|
||||
.ok_or_else(|| SpreadsheetArtifactError::InvalidAddress {
|
||||
address: column.to_string(),
|
||||
message: "column is too large".to_string(),
|
||||
})?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn column_index_to_letters(mut index: u32) -> String {
|
||||
if index == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut letters = Vec::new();
|
||||
while index > 0 {
|
||||
let remainder = (index - 1) % 26;
|
||||
letters.push((b'A' + remainder as u8) as char);
|
||||
index = (index - 1) / 26;
|
||||
}
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
pub fn parse_column_reference(reference: &str) -> Result<(u32, u32), SpreadsheetArtifactError> {
|
||||
let trimmed = reference.trim();
|
||||
if let Some((left, right)) = trimmed.split_once(':') {
|
||||
let start = column_letters_to_index(left)?;
|
||||
let end = column_letters_to_index(right)?;
|
||||
Ok((start.min(end), start.max(end)))
|
||||
} else {
|
||||
let column = column_letters_to_index(trimmed)?;
|
||||
Ok((column, column))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_cell_reference(address: &str) -> bool {
|
||||
CellAddress::parse(address).is_ok()
|
||||
}
|
||||
|
||||
pub fn is_valid_range_reference(address: &str) -> bool {
|
||||
CellRange::parse(address).is_ok()
|
||||
}
|
||||
|
||||
pub fn is_valid_row_reference(address: &str) -> bool {
|
||||
CellRange::parse(address)
|
||||
.map(|range| range.is_single_row())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn is_valid_column_reference(address: &str) -> bool {
|
||||
parse_column_reference(address).is_ok()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SpreadsheetArtifactError {
|
||||
#[error("missing `artifact_id` for action `{action}`")]
|
||||
MissingArtifactId { action: String },
|
||||
#[error("unknown artifact id `{artifact_id}` for action `{action}`")]
|
||||
UnknownArtifactId { action: String, artifact_id: String },
|
||||
#[error("unknown action `{0}`")]
|
||||
UnknownAction(String),
|
||||
#[error("invalid args for action `{action}`: {message}")]
|
||||
InvalidArgs { action: String, message: String },
|
||||
#[error("invalid address `{address}`: {message}")]
|
||||
InvalidAddress { address: String, message: String },
|
||||
#[error("sheet lookup failed for action `{action}`: {message}")]
|
||||
SheetLookup { action: String, message: String },
|
||||
#[error("index `{index}` is out of range for action `{action}`; len={len}")]
|
||||
IndexOutOfRange {
|
||||
action: String,
|
||||
index: usize,
|
||||
len: usize,
|
||||
},
|
||||
#[error("merge conflict for action `{action}` on range `{range}` with `{conflict}`")]
|
||||
MergeConflict {
|
||||
action: String,
|
||||
range: String,
|
||||
conflict: String,
|
||||
},
|
||||
#[error("formula error at `{location}`: {message}")]
|
||||
Formula { location: String, message: String },
|
||||
#[error("serialization failed: {message}")]
|
||||
Serialization { message: String },
|
||||
#[error("failed to import XLSX `{path}`: {message}")]
|
||||
ImportFailed { path: PathBuf, message: String },
|
||||
#[error("failed to export XLSX `{path}`: {message}")]
|
||||
ExportFailed { path: PathBuf, message: String },
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::CellAddress;
|
||||
use crate::CellRange;
|
||||
use crate::SpreadsheetArtifact;
|
||||
use crate::SpreadsheetArtifactError;
|
||||
use crate::SpreadsheetCellValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Token {
|
||||
Number(f64),
|
||||
Cell(String),
|
||||
Ident(String),
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
LParen,
|
||||
RParen,
|
||||
Colon,
|
||||
Comma,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Expr {
|
||||
Number(f64),
|
||||
Cell(CellAddress),
|
||||
Range(CellRange),
|
||||
UnaryMinus(Box<Expr>),
|
||||
Binary {
|
||||
op: BinaryOp,
|
||||
left: Box<Expr>,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
Function {
|
||||
name: String,
|
||||
args: Vec<Expr>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum BinaryOp {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum EvalValue {
|
||||
Scalar(Option<SpreadsheetCellValue>),
|
||||
Range(Vec<Option<SpreadsheetCellValue>>),
|
||||
}
|
||||
|
||||
pub(crate) fn recalculate_workbook(artifact: &mut SpreadsheetArtifact) {
|
||||
let updates = artifact
|
||||
.sheets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(sheet_index, sheet)| {
|
||||
sheet.cells.iter().filter_map(move |(address, cell)| {
|
||||
cell.formula
|
||||
.as_ref()
|
||||
.map(|formula| (sheet_index, *address, formula.clone()))
|
||||
})
|
||||
})
|
||||
.map(|(sheet_index, address, formula)| {
|
||||
let mut stack = BTreeSet::new();
|
||||
let value = evaluate_formula(artifact, sheet_index, &formula, &mut stack)
|
||||
.unwrap_or_else(|error| {
|
||||
Some(SpreadsheetCellValue::Error(map_error_to_code(&error)))
|
||||
});
|
||||
(sheet_index, address, value)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (sheet_index, address, value) in updates {
|
||||
if let Some(sheet) = artifact.sheets.get_mut(sheet_index)
|
||||
&& let Some(cell) = sheet.cells.get_mut(&address)
|
||||
{
|
||||
cell.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_formula(
|
||||
artifact: &SpreadsheetArtifact,
|
||||
sheet_index: usize,
|
||||
formula: &str,
|
||||
stack: &mut BTreeSet<(usize, CellAddress)>,
|
||||
) -> Result<Option<SpreadsheetCellValue>, SpreadsheetArtifactError> {
|
||||
let source = formula.trim().trim_start_matches('=');
|
||||
let tokens = tokenize(source)?;
|
||||
let mut parser = Parser::new(tokens);
|
||||
let expr = parser.parse_expression()?;
|
||||
if parser.has_remaining() {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: formula.to_string(),
|
||||
message: "unexpected trailing tokens".to_string(),
|
||||
});
|
||||
}
|
||||
match evaluate_expr(artifact, sheet_index, &expr, stack)? {
|
||||
EvalValue::Scalar(value) => Ok(value),
|
||||
EvalValue::Range(_) => Err(SpreadsheetArtifactError::Formula {
|
||||
location: formula.to_string(),
|
||||
message: "range expressions are only allowed inside functions".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_expr(
|
||||
artifact: &SpreadsheetArtifact,
|
||||
sheet_index: usize,
|
||||
expr: &Expr,
|
||||
stack: &mut BTreeSet<(usize, CellAddress)>,
|
||||
) -> Result<EvalValue, SpreadsheetArtifactError> {
|
||||
match expr {
|
||||
Expr::Number(value) => Ok(EvalValue::Scalar(Some(number_to_value(*value)))),
|
||||
Expr::Cell(address) => evaluate_cell_reference(artifact, sheet_index, *address, stack),
|
||||
Expr::Range(range) => {
|
||||
let sheet = artifact.sheets.get(sheet_index).ok_or_else(|| {
|
||||
SpreadsheetArtifactError::Formula {
|
||||
location: range.to_a1(),
|
||||
message: "sheet index was not found".to_string(),
|
||||
}
|
||||
})?;
|
||||
let values = range
|
||||
.addresses()
|
||||
.map(|address| sheet.get_cell(address).and_then(|cell| cell.value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
Ok(EvalValue::Range(values))
|
||||
}
|
||||
Expr::UnaryMinus(inner) => {
|
||||
let value = evaluate_scalar(artifact, sheet_index, inner, stack)?;
|
||||
Ok(EvalValue::Scalar(match value {
|
||||
None => Some(SpreadsheetCellValue::Integer(0)),
|
||||
Some(SpreadsheetCellValue::Integer(value)) => {
|
||||
Some(SpreadsheetCellValue::Integer(-value))
|
||||
}
|
||||
Some(SpreadsheetCellValue::Float(value)) => {
|
||||
Some(SpreadsheetCellValue::Float(-value))
|
||||
}
|
||||
Some(SpreadsheetCellValue::Error(value)) => {
|
||||
Some(SpreadsheetCellValue::Error(value))
|
||||
}
|
||||
Some(_) => Some(SpreadsheetCellValue::Error("#VALUE!".to_string())),
|
||||
}))
|
||||
}
|
||||
Expr::Binary { op, left, right } => {
|
||||
let left = evaluate_scalar(artifact, sheet_index, left, stack)?;
|
||||
let right = evaluate_scalar(artifact, sheet_index, right, stack)?;
|
||||
Ok(EvalValue::Scalar(Some(apply_binary_op(*op, left, right)?)))
|
||||
}
|
||||
Expr::Function { name, args } => {
|
||||
let mut numeric = Vec::new();
|
||||
for arg in args {
|
||||
match evaluate_expr(artifact, sheet_index, arg, stack)? {
|
||||
EvalValue::Scalar(value) => {
|
||||
if let Some(number) = scalar_to_number(value.clone())? {
|
||||
numeric.push(number);
|
||||
}
|
||||
}
|
||||
EvalValue::Range(values) => {
|
||||
for value in values {
|
||||
if let Some(number) = scalar_to_number(value.clone())? {
|
||||
numeric.push(number);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let upper = name.to_ascii_uppercase();
|
||||
let result = match upper.as_str() {
|
||||
"SUM" => numeric.iter().sum::<f64>(),
|
||||
"AVERAGE" => {
|
||||
if numeric.is_empty() {
|
||||
return Ok(EvalValue::Scalar(None));
|
||||
}
|
||||
numeric.iter().sum::<f64>() / numeric.len() as f64
|
||||
}
|
||||
"MIN" => numeric.iter().copied().reduce(f64::min).unwrap_or(0.0),
|
||||
"MAX" => numeric.iter().copied().reduce(f64::max).unwrap_or(0.0),
|
||||
_ => {
|
||||
return Ok(EvalValue::Scalar(Some(SpreadsheetCellValue::Error(
|
||||
"#NAME?".to_string(),
|
||||
))));
|
||||
}
|
||||
};
|
||||
Ok(EvalValue::Scalar(Some(number_to_value(result))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_scalar(
|
||||
artifact: &SpreadsheetArtifact,
|
||||
sheet_index: usize,
|
||||
expr: &Expr,
|
||||
stack: &mut BTreeSet<(usize, CellAddress)>,
|
||||
) -> Result<Option<SpreadsheetCellValue>, SpreadsheetArtifactError> {
|
||||
match evaluate_expr(artifact, sheet_index, expr, stack)? {
|
||||
EvalValue::Scalar(value) => Ok(value),
|
||||
EvalValue::Range(_) => Err(SpreadsheetArtifactError::Formula {
|
||||
location: format!("{expr:?}"),
|
||||
message: "expected a scalar expression".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_cell_reference(
|
||||
artifact: &SpreadsheetArtifact,
|
||||
sheet_index: usize,
|
||||
address: CellAddress,
|
||||
stack: &mut BTreeSet<(usize, CellAddress)>,
|
||||
) -> Result<EvalValue, SpreadsheetArtifactError> {
|
||||
let Some(sheet) = artifact.sheets.get(sheet_index) else {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: address.to_a1(),
|
||||
message: "sheet index was not found".to_string(),
|
||||
});
|
||||
};
|
||||
let key = (sheet_index, address);
|
||||
if !stack.insert(key) {
|
||||
return Ok(EvalValue::Scalar(Some(SpreadsheetCellValue::Error(
|
||||
"#CYCLE!".to_string(),
|
||||
))));
|
||||
}
|
||||
|
||||
let value = if let Some(cell) = sheet.get_cell(address) {
|
||||
if let Some(formula) = &cell.formula {
|
||||
evaluate_formula(artifact, sheet_index, formula, stack)?
|
||||
} else {
|
||||
cell.value.clone()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
stack.remove(&key);
|
||||
Ok(EvalValue::Scalar(value))
|
||||
}
|
||||
|
||||
fn apply_binary_op(
|
||||
op: BinaryOp,
|
||||
left: Option<SpreadsheetCellValue>,
|
||||
right: Option<SpreadsheetCellValue>,
|
||||
) -> Result<SpreadsheetCellValue, SpreadsheetArtifactError> {
|
||||
if let Some(SpreadsheetCellValue::Error(value)) = &left {
|
||||
return Ok(SpreadsheetCellValue::Error(value.clone()));
|
||||
}
|
||||
if let Some(SpreadsheetCellValue::Error(value)) = &right {
|
||||
return Ok(SpreadsheetCellValue::Error(value.clone()));
|
||||
}
|
||||
|
||||
let left = scalar_to_number(left)?;
|
||||
let right = scalar_to_number(right)?;
|
||||
let left = left.unwrap_or(0.0);
|
||||
let right = right.unwrap_or(0.0);
|
||||
let result = match op {
|
||||
BinaryOp::Add => left + right,
|
||||
BinaryOp::Subtract => left - right,
|
||||
BinaryOp::Multiply => left * right,
|
||||
BinaryOp::Divide => {
|
||||
if right == 0.0 {
|
||||
return Ok(SpreadsheetCellValue::Error("#DIV/0!".to_string()));
|
||||
}
|
||||
left / right
|
||||
}
|
||||
};
|
||||
Ok(number_to_value(result))
|
||||
}
|
||||
|
||||
fn scalar_to_number(
|
||||
value: Option<SpreadsheetCellValue>,
|
||||
) -> Result<Option<f64>, SpreadsheetArtifactError> {
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(SpreadsheetCellValue::Integer(value)) => Ok(Some(value as f64)),
|
||||
Some(SpreadsheetCellValue::Float(value)) => Ok(Some(value)),
|
||||
Some(SpreadsheetCellValue::Bool(value)) => Ok(Some(if value { 1.0 } else { 0.0 })),
|
||||
Some(SpreadsheetCellValue::Error(value)) => Err(SpreadsheetArtifactError::Formula {
|
||||
location: value,
|
||||
message: "encountered error value".to_string(),
|
||||
}),
|
||||
Some(other) => Err(SpreadsheetArtifactError::Formula {
|
||||
location: format!("{other:?}"),
|
||||
message: "value is not numeric".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn number_to_value(number: f64) -> SpreadsheetCellValue {
|
||||
if number.fract() == 0.0 {
|
||||
SpreadsheetCellValue::Integer(number as i64)
|
||||
} else {
|
||||
SpreadsheetCellValue::Float(number)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error_to_code(error: &SpreadsheetArtifactError) -> String {
|
||||
match error {
|
||||
SpreadsheetArtifactError::Formula { message, .. } => {
|
||||
if message.contains("cycle") {
|
||||
"#CYCLE!".to_string()
|
||||
} else if message.contains("not numeric") || message.contains("scalar") {
|
||||
"#VALUE!".to_string()
|
||||
} else {
|
||||
"#ERROR!".to_string()
|
||||
}
|
||||
}
|
||||
SpreadsheetArtifactError::InvalidAddress { .. } => "#REF!".to_string(),
|
||||
_ => "#ERROR!".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tokenize(source: &str) -> Result<Vec<Token>, SpreadsheetArtifactError> {
|
||||
let chars = source.chars().collect::<Vec<_>>();
|
||||
let mut index = 0usize;
|
||||
let mut tokens = Vec::new();
|
||||
while index < chars.len() {
|
||||
let ch = chars[index];
|
||||
if ch.is_ascii_whitespace() {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
match ch {
|
||||
'+' => {
|
||||
tokens.push(Token::Plus);
|
||||
index += 1;
|
||||
}
|
||||
'-' => {
|
||||
tokens.push(Token::Minus);
|
||||
index += 1;
|
||||
}
|
||||
'*' => {
|
||||
tokens.push(Token::Star);
|
||||
index += 1;
|
||||
}
|
||||
'/' => {
|
||||
tokens.push(Token::Slash);
|
||||
index += 1;
|
||||
}
|
||||
'(' => {
|
||||
tokens.push(Token::LParen);
|
||||
index += 1;
|
||||
}
|
||||
')' => {
|
||||
tokens.push(Token::RParen);
|
||||
index += 1;
|
||||
}
|
||||
':' => {
|
||||
tokens.push(Token::Colon);
|
||||
index += 1;
|
||||
}
|
||||
',' => {
|
||||
tokens.push(Token::Comma);
|
||||
index += 1;
|
||||
}
|
||||
'0'..='9' | '.' => {
|
||||
let start = index;
|
||||
index += 1;
|
||||
while index < chars.len() && (chars[index].is_ascii_digit() || chars[index] == '.')
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
let number = source[start..index].parse::<f64>().map_err(|_| {
|
||||
SpreadsheetArtifactError::Formula {
|
||||
location: source.to_string(),
|
||||
message: "invalid numeric literal".to_string(),
|
||||
}
|
||||
})?;
|
||||
tokens.push(Token::Number(number));
|
||||
}
|
||||
'A'..='Z' | 'a'..='z' | '_' => {
|
||||
let start = index;
|
||||
index += 1;
|
||||
while index < chars.len()
|
||||
&& (chars[index].is_ascii_alphanumeric() || chars[index] == '_')
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
let text = source[start..index].to_string();
|
||||
if text.chars().any(|part| part.is_ascii_digit())
|
||||
&& text.chars().any(|part| part.is_ascii_alphabetic())
|
||||
{
|
||||
tokens.push(Token::Cell(text));
|
||||
} else {
|
||||
tokens.push(Token::Ident(text));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: source.to_string(),
|
||||
message: format!("unsupported token `{other}`"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn new(tokens: Vec<Token>) -> Self {
|
||||
Self { tokens, index: 0 }
|
||||
}
|
||||
|
||||
fn has_remaining(&self) -> bool {
|
||||
self.index < self.tokens.len()
|
||||
}
|
||||
|
||||
fn parse_expression(&mut self) -> Result<Expr, SpreadsheetArtifactError> {
|
||||
let mut expr = self.parse_term()?;
|
||||
while let Some(token) = self.peek() {
|
||||
let op = match token {
|
||||
Token::Plus => BinaryOp::Add,
|
||||
Token::Minus => BinaryOp::Subtract,
|
||||
_ => break,
|
||||
};
|
||||
self.index += 1;
|
||||
let right = self.parse_term()?;
|
||||
expr = Expr::Binary {
|
||||
op,
|
||||
left: Box::new(expr),
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn parse_term(&mut self) -> Result<Expr, SpreadsheetArtifactError> {
|
||||
let mut expr = self.parse_factor()?;
|
||||
while let Some(token) = self.peek() {
|
||||
let op = match token {
|
||||
Token::Star => BinaryOp::Multiply,
|
||||
Token::Slash => BinaryOp::Divide,
|
||||
_ => break,
|
||||
};
|
||||
self.index += 1;
|
||||
let right = self.parse_factor()?;
|
||||
expr = Expr::Binary {
|
||||
op,
|
||||
left: Box::new(expr),
|
||||
right: Box::new(right),
|
||||
};
|
||||
}
|
||||
Ok(expr)
|
||||
}
|
||||
|
||||
fn parse_factor(&mut self) -> Result<Expr, SpreadsheetArtifactError> {
|
||||
match self.peek() {
|
||||
Some(Token::Minus) => {
|
||||
self.index += 1;
|
||||
Ok(Expr::UnaryMinus(Box::new(self.parse_factor()?)))
|
||||
}
|
||||
_ => self.parse_primary(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Result<Expr, SpreadsheetArtifactError> {
|
||||
match self.next().cloned() {
|
||||
Some(Token::Number(value)) => Ok(Expr::Number(value)),
|
||||
Some(Token::Cell(address)) => {
|
||||
let start = CellAddress::parse(&address)?;
|
||||
if matches!(self.peek(), Some(Token::Colon)) {
|
||||
self.index += 1;
|
||||
let Some(Token::Cell(end)) = self.next().cloned() else {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: address,
|
||||
message: "expected cell after `:`".to_string(),
|
||||
});
|
||||
};
|
||||
Ok(Expr::Range(CellRange::from_start_end(
|
||||
start,
|
||||
CellAddress::parse(&end)?,
|
||||
)))
|
||||
} else {
|
||||
Ok(Expr::Cell(start))
|
||||
}
|
||||
}
|
||||
Some(Token::Ident(name)) => {
|
||||
if !matches!(self.next(), Some(Token::LParen)) {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: name,
|
||||
message: "expected `(` after function name".to_string(),
|
||||
});
|
||||
}
|
||||
let mut args = Vec::new();
|
||||
if !matches!(self.peek(), Some(Token::RParen)) {
|
||||
loop {
|
||||
args.push(self.parse_expression()?);
|
||||
if matches!(self.peek(), Some(Token::Comma)) {
|
||||
self.index += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !matches!(self.next(), Some(Token::RParen)) {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: name,
|
||||
message: "expected `)`".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(Expr::Function { name, args })
|
||||
}
|
||||
Some(Token::LParen) => {
|
||||
let expr = self.parse_expression()?;
|
||||
if !matches!(self.next(), Some(Token::RParen)) {
|
||||
return Err(SpreadsheetArtifactError::Formula {
|
||||
location: format!("{expr:?}"),
|
||||
message: "expected `)`".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(expr)
|
||||
}
|
||||
other => Err(SpreadsheetArtifactError::Formula {
|
||||
location: format!("{other:?}"),
|
||||
message: "unexpected token".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn peek(&self) -> Option<&Token> {
|
||||
self.tokens.get(self.index)
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<&Token> {
|
||||
let token = self.tokens.get(self.index);
|
||||
self.index += usize::from(token.is_some());
|
||||
token
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user