Skip to content

Commit 936281d

Browse files
authored
Merge pull request #7 from On-Behalf-AI/merge/main-into-feat-skills-2026-05-14
merge: integrate main (storage_session_id contract alignment) into feat/agent-skills-runtime
2 parents b9d5681 + 6877fde commit 936281d

22 files changed

Lines changed: 259 additions & 101 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ Please see [SECURITY.md](docs/SECURITY.md) for our security policy and reporting
228228

229229
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started, our code of conduct, and the pull request process.
230230

231+
## Star History
232+
233+
[![Star History Chart](https://api.star-history.com/svg?repos=usnavy13/LibreCodeInterpreter&type=Date)](https://star-history.com/#usnavy13/LibreCodeInterpreter&Date)
234+
231235
## License
232236

233237
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

src/api/files.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ async def upload_file(
172172
# Note: Production API returns different format with fileId instead of id
173173
return {
174174
"message": "success",
175+
"storage_session_id": session_id,
175176
"session_id": session_id,
176177
"files": [
177178
{"filename": file["name"], "fileId": file["id"]}
@@ -249,7 +250,10 @@ async def upload_files_batch(
249250
entity_id: Optional[str] = (
250251
entity_id_raw if isinstance(entity_id_raw, str) and entity_id_raw else None
251252
)
252-
is_agent_file = entity_id is not None
253+
kind_raw = form.get("kind")
254+
is_agent_file = entity_id is not None or (
255+
isinstance(kind_raw, str) and kind_raw in ("skill", "agent")
256+
)
253257

254258
read_only_raw = form.get("read_only")
255259
is_read_only = isinstance(read_only_raw, str) and read_only_raw.lower() in (
@@ -339,6 +343,7 @@ async def upload_files_batch(
339343

340344
return {
341345
"message": message,
346+
"storage_session_id": session_id,
342347
"session_id": session_id,
343348
"files": results,
344349
"succeeded": succeeded,
@@ -353,6 +358,18 @@ async def list_files(
353358
None,
354359
description="Detail level: 'simple' for basic info, otherwise full details",
355360
),
361+
kind: Optional[str] = Query(
362+
None,
363+
description="Resource kind filter: 'skill', 'agent', or 'user'",
364+
),
365+
id: Optional[str] = Query(
366+
None,
367+
description="Resource id for scoped file listing",
368+
),
369+
version: Optional[int] = Query(
370+
None,
371+
description="Resource version (only meaningful when kind=skill)",
372+
),
356373
file_service: FileServiceDep = None,
357374
):
358375
"""List all files in a session with optional detail parameter - LibreChat compatible."""
@@ -408,6 +425,7 @@ async def list_files(
408425
{
409426
"name": f"{session_id}/{file_info.file_id}",
410427
"id": file_info.file_id,
428+
"storage_session_id": session_id,
411429
"session_id": session_id,
412430
"content": None, # Not returned in list
413431
"size": file_info.size,

src/models/exec.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,45 @@
55
from typing import Dict, List, Optional, Any
66

77
# Third-party imports
8-
from pydantic import BaseModel, Field
8+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field
99

1010

1111
class FileRef(BaseModel):
1212
"""File reference model for execution response."""
1313

14+
model_config = ConfigDict(populate_by_name=True)
15+
1416
id: str
1517
name: str
16-
path: Optional[str] = None # Make path optional
17-
session_id: Optional[str] = None # Session ID for cross-message file persistence
18+
path: Optional[str] = None
19+
session_id: Optional[str] = None
1820
inherited: Optional[bool] = None
1921
entity_id: Optional[str] = None
22+
resource_id: Optional[str] = None
23+
kind: Optional[str] = None
24+
version: Optional[int] = None
2025
modified_from: Optional[Dict[str, str]] = None
2126

27+
@computed_field # type: ignore[prop-decorator]
28+
@property
29+
def storage_session_id(self) -> Optional[str]:
30+
return self.session_id
31+
2232

2333
class RequestFile(BaseModel):
2434
"""Request file model."""
2535

36+
model_config = ConfigDict(populate_by_name=True)
37+
2638
id: str
27-
session_id: str
39+
session_id: str = Field(
40+
validation_alias=AliasChoices("storage_session_id", "session_id"),
41+
)
2842
name: str
2943
entity_id: Optional[str] = None
44+
resource_id: Optional[str] = None
45+
kind: Optional[str] = None
46+
version: Optional[int] = None
3047

3148

3249
class ExecRequest(BaseModel):

src/models/programmatic.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from typing import Any, Dict, List, Optional
1010

11-
from pydantic import BaseModel, Field, validator
11+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, validator
1212

1313
SUPPORTED_PTC_LANGUAGES = {"py", "bash"}
1414

@@ -51,12 +51,21 @@ class PTCFileInput(BaseModel):
5151
"""File payload for PTC initial execution.
5252
5353
Matches the LibreChat/librechat-agents CodeEnvFile shape:
54-
{session_id, id, name}
54+
{storage_session_id, id, name}
5555
"""
5656

57+
model_config = ConfigDict(populate_by_name=True)
58+
5759
id: str = Field(..., description="File identifier")
5860
name: str = Field(..., description="Original filename for the referenced file")
59-
session_id: str = Field(..., description="Source session for a referenced file")
61+
session_id: str = Field(
62+
...,
63+
description="Source session for a referenced file",
64+
validation_alias=AliasChoices("storage_session_id", "session_id"),
65+
)
66+
resource_id: Optional[str] = None
67+
kind: Optional[str] = None
68+
version: Optional[int] = None
6069

6170

6271
class ProgrammaticExecRequest(BaseModel):

src/services/orchestrator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,8 @@ async def _handle_generated_files(self, ctx: ExecutionContext) -> List[FileRef]:
786786
if meta.get("modified_from_id"):
787787
file_ref.modified_from = {
788788
"id": meta["modified_from_id"],
789-
"session_id": meta.get("modified_from_session_id") or "",
789+
"storage_session_id": meta.get("modified_from_session_id")
790+
or "",
790791
}
791792
generated.append(file_ref)
792793
logger.debug(

tests/functional/test_client_replay.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def _normalize_artifact_files(result: dict) -> list[dict]:
1212
session_id = result["session_id"]
1313
return [
1414
{
15-
"session_id": file_info.get("session_id") or session_id,
15+
"storage_session_id": file_info.get("storage_session_id") or session_id,
1616
"id": file_info["id"],
1717
"name": file_info["name"],
1818
}
@@ -36,7 +36,7 @@ async def _fetch_runtime_file_refs(
3636
file_id = name_parts[1].split(".")[0] if len(name_parts) > 1 else ""
3737
file_references.append(
3838
{
39-
"session_id": session_id,
39+
"storage_session_id": session_id,
4040
"id": file_id,
4141
"name": file_info["metadata"]["original-filename"],
4242
}
@@ -128,7 +128,7 @@ async def test_uploaded_files_follow_runtime_session_when_first_exec_has_no_outp
128128
data={"entity_id": unique_entity_id},
129129
)
130130
assert upload.status_code == 200, upload.text
131-
upload_session_id = upload.json()["session_id"]
131+
upload_session_id = upload.json()["storage_session_id"]
132132
upload_refs = await _fetch_runtime_file_refs(
133133
async_client, auth_headers, upload_session_id
134134
)
@@ -164,7 +164,7 @@ async def test_uploaded_files_survive_runtime_fallback_after_outputs_are_generat
164164
data={"entity_id": unique_entity_id},
165165
)
166166
assert upload.status_code == 200, upload.text
167-
upload_session_id = upload.json()["session_id"]
167+
upload_session_id = upload.json()["storage_session_id"]
168168
upload_refs = await _fetch_runtime_file_refs(
169169
async_client, auth_headers, upload_session_id
170170
)
@@ -298,7 +298,7 @@ async def test_ptc_fallback_refs_preserve_files_after_continuation(
298298
)
299299
assert upload.status_code == 200, upload.text
300300
upload_result = upload.json()
301-
session_id = upload_result["session_id"]
301+
session_id = upload_result["storage_session_id"]
302302

303303
initial = await _start_ptc_like_runtime(
304304
async_client,

tests/functional/test_concurrent_file_exec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def test_large_file_exec_does_not_block_concurrent_requests(
5656
assert upload_resp.status_code == 200, f"Upload failed: {upload_resp.text}"
5757

5858
result = upload_resp.json()
59-
session_id = result["session_id"]
59+
session_id = result["storage_session_id"]
6060
file_id = result["files"][0]["fileId"]
6161
filename = result["files"][0]["filename"]
6262

@@ -72,7 +72,7 @@ async def exec_with_file(idx: int) -> tuple:
7272
"lang": "py",
7373
"session_id": session_id,
7474
"files": [
75-
{"id": file_id, "session_id": session_id, "name": filename}
75+
{"id": file_id, "storage_session_id": session_id, "name": filename}
7676
],
7777
},
7878
timeout=60.0,

tests/functional/test_exec_workflow.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ async def test_file_ref_does_not_leak_session_across_users(
172172
)
173173
assert upload.status_code == 200
174174
upload_data = upload.json()
175-
upload_session = upload_data["session_id"]
175+
upload_session = upload_data["storage_session_id"]
176176
file_id = upload_data["files"][0]["fileId"]
177177
filename = upload_data["files"][0]["filename"]
178178

@@ -187,7 +187,7 @@ async def test_file_ref_does_not_leak_session_across_users(
187187
"files": [
188188
{
189189
"id": file_id,
190-
"session_id": upload_session,
190+
"storage_session_id": upload_session,
191191
"name": filename,
192192
}
193193
],
@@ -208,7 +208,7 @@ async def test_file_ref_does_not_leak_session_across_users(
208208
"files": [
209209
{
210210
"id": file_id,
211-
"session_id": upload_session,
211+
"storage_session_id": upload_session,
212212
"name": filename,
213213
}
214214
],

tests/functional/test_files.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async def test_upload_single_file(
2525
result = response.json()
2626

2727
assert result["message"] == "success"
28-
assert "session_id" in result
28+
assert "storage_session_id" in result
2929
assert len(result["files"]) == 1
3030
assert "fileId" in result["files"][0]
3131
assert "filename" in result["files"][0]
@@ -50,10 +50,10 @@ async def test_librechat_upload_format(
5050
assert response.json()["message"] == "success"
5151

5252
@pytest.mark.asyncio
53-
async def test_upload_returns_session_id(
53+
async def test_upload_returns_storage_session_id(
5454
self, async_client, auth_headers, unique_entity_id
5555
):
56-
"""Upload response includes session_id."""
56+
"""Upload response includes storage_session_id."""
5757
files = {"files": ("test.txt", b"content", "text/plain")}
5858
data = {"entity_id": unique_entity_id}
5959

@@ -65,8 +65,8 @@ async def test_upload_returns_session_id(
6565
)
6666

6767
result = response.json()
68-
assert "session_id" in result
69-
assert len(result["session_id"]) > 0
68+
assert "storage_session_id" in result
69+
assert len(result["storage_session_id"]) > 0
7070

7171
@pytest.mark.asyncio
7272
async def test_upload_returns_file_info(
@@ -120,7 +120,7 @@ async def test_list_files_after_upload(
120120
files=files,
121121
data={"entity_id": unique_entity_id},
122122
)
123-
session_id = upload.json()["session_id"]
123+
session_id = upload.json()["storage_session_id"]
124124

125125
# List files
126126
response = await async_client.get(
@@ -145,7 +145,7 @@ async def test_list_files_detail_simple(
145145
files=files,
146146
data={"entity_id": unique_entity_id},
147147
)
148-
session_id = upload.json()["session_id"]
148+
session_id = upload.json()["storage_session_id"]
149149

150150
# List with simple detail
151151
response = await async_client.get(
@@ -170,7 +170,7 @@ async def test_list_files_detail_summary(
170170
files=files,
171171
data={"entity_id": unique_entity_id},
172172
)
173-
session_id = upload.json()["session_id"]
173+
session_id = upload.json()["storage_session_id"]
174174

175175
# List with summary detail
176176
response = await async_client.get(
@@ -204,7 +204,7 @@ async def test_detail_full_has_original_filename_metadata(
204204
data={"entity_id": unique_entity_id},
205205
)
206206
assert upload.status_code == 200
207-
session_id = upload.json()["session_id"]
207+
session_id = upload.json()["storage_session_id"]
208208

209209
# Get full detail
210210
response = await async_client.get(
@@ -237,7 +237,7 @@ async def test_detail_full_has_required_fields(
237237
files=files,
238238
data={"entity_id": unique_entity_id},
239239
)
240-
session_id = upload.json()["session_id"]
240+
session_id = upload.json()["storage_session_id"]
241241

242242
response = await async_client.get(
243243
f"/files/{session_id}?detail=full",
@@ -278,7 +278,7 @@ async def test_download_uploaded_file(
278278
data={"entity_id": unique_entity_id},
279279
)
280280

281-
session_id = upload.json()["session_id"]
281+
session_id = upload.json()["storage_session_id"]
282282
file_id = upload.json()["files"][0]["fileId"]
283283

284284
response = await async_client.get(
@@ -322,7 +322,7 @@ async def test_uploaded_file_readable_at_mnt_data(
322322
)
323323
assert upload.status_code == 200
324324
upload_data = upload.json()
325-
session_id = upload_data["session_id"]
325+
session_id = upload_data["storage_session_id"]
326326
file_id = upload_data["files"][0]["fileId"]
327327
filename = upload_data["files"][0]["filename"]
328328

@@ -341,7 +341,7 @@ async def test_uploaded_file_readable_at_mnt_data(
341341
),
342342
"lang": "py",
343343
"session_id": session_id,
344-
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
344+
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
345345
},
346346
)
347347

@@ -366,7 +366,7 @@ async def test_uploaded_file_readable_via_relative_path(
366366
data={"entity_id": unique_entity_id},
367367
)
368368
upload_data = upload.json()
369-
session_id = upload_data["session_id"]
369+
session_id = upload_data["storage_session_id"]
370370
file_id = upload_data["files"][0]["fileId"]
371371
filename = upload_data["files"][0]["filename"]
372372

@@ -377,7 +377,7 @@ async def test_uploaded_file_readable_via_relative_path(
377377
"code": f"print(open('{filename}').read())",
378378
"lang": "py",
379379
"session_id": session_id,
380-
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
380+
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
381381
},
382382
)
383383

@@ -400,7 +400,7 @@ async def test_upload_execute_generate_download(
400400
data={"entity_id": unique_entity_id},
401401
)
402402
upload_data = upload.json()
403-
session_id = upload_data["session_id"]
403+
session_id = upload_data["storage_session_id"]
404404
file_id = upload_data["files"][0]["fileId"]
405405
filename = upload_data["files"][0]["filename"]
406406

@@ -424,7 +424,7 @@ async def test_upload_execute_generate_download(
424424
),
425425
"lang": "py",
426426
"session_id": session_id,
427-
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
427+
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
428428
},
429429
)
430430

tests/functional/test_generated_artifacts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ async def test_generated_file_is_reused_on_follow_up_execution(
114114
"files": [
115115
{
116116
"id": generated_file["id"],
117-
"session_id": generate_result["session_id"],
117+
"storage_session_id": generate_result["session_id"],
118118
"name": generated_file["name"],
119119
}
120120
],

0 commit comments

Comments
 (0)