agentram 0.1.21 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -146
- package/agentram/cli.py +373 -71
- package/agentram/orchestration.py +159 -28
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# AgentRAM
|
|
1
|
+
# AgentRAM
|
|
2
2
|
|
|
3
3
|
Async, model-agnostic Working RAM for coding agents such as Codex, Claude Code, Cursor, and custom MCP clients.
|
|
4
4
|
|
|
@@ -358,20 +358,20 @@ Minimum usage pattern:
|
|
|
358
358
|
|
|
359
359
|
AgentRAM can bind multiple coding model endpoints and let Claude Code or MCP clients orchestrate them in parallel. This keeps the `free-code` style command registry, but adds project RAM, Goal Stack, and multi-agent model routing.
|
|
360
360
|
|
|
361
|
-
Bind models:
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
agentram bind-model claude-subagent agentram-reviewer --role reviewer
|
|
365
|
-
agentram bind-model claude claude-3-5-sonnet-latest --role reviewer --api-key-env ANTHROPIC_API_KEY
|
|
366
|
-
agentram bind-model openai-compatible qwen2.5-coder --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Claude Code subagent endpoint uses your local Claude CLI entrypoint. `model` is the subagent name from `.claude/agents`, for example `agentram-reviewer` or `agentram-planner`. Default command is `claude -p`. Override command when needed:
|
|
370
|
-
|
|
371
|
-
```bash
|
|
372
|
-
set AGENTRAM_CLAUDE_COMMAND=claude
|
|
373
|
-
agentram bind-model claude-subagent agentram-reviewer --role reviewer
|
|
374
|
-
```
|
|
361
|
+
Bind models:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
agentram bind-model claude-subagent agentram-reviewer --role reviewer
|
|
365
|
+
agentram bind-model claude claude-3-5-sonnet-latest --role reviewer --api-key-env ANTHROPIC_API_KEY
|
|
366
|
+
agentram bind-model openai-compatible qwen2.5-coder --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Claude Code subagent endpoint uses your local Claude CLI entrypoint. `model` is the subagent name from `.claude/agents`, for example `agentram-reviewer` or `agentram-planner`. Default command is `claude -p`. Override command when needed:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
set AGENTRAM_CLAUDE_COMMAND=claude
|
|
373
|
+
agentram bind-model claude-subagent agentram-reviewer --role reviewer
|
|
374
|
+
```
|
|
375
375
|
|
|
376
376
|
Build a dry-run orchestration plan:
|
|
377
377
|
|
|
@@ -396,12 +396,12 @@ agentram_run_coding_orchestration
|
|
|
396
396
|
|
|
397
397
|
TUI slash commands. Plain text only records/retrieves RAM; use `/ask` to call bound models:
|
|
398
398
|
|
|
399
|
-
```text
|
|
400
|
-
/bind-model claude-subagent agentram-reviewer role=reviewer
|
|
401
|
-
/bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY
|
|
402
|
-
/models
|
|
403
|
-
/orchestrate review CLI orchestration
|
|
404
|
-
/ask xin
|
|
399
|
+
```text
|
|
400
|
+
/bind-model claude-subagent agentram-reviewer role=reviewer
|
|
401
|
+
/bind-model claude claude-3-5-sonnet-latest role=reviewer api_key_env=ANTHROPIC_API_KEY
|
|
402
|
+
/models
|
|
403
|
+
/orchestrate review CLI orchestration
|
|
404
|
+
/ask xin chà o
|
|
405
405
|
```
|
|
406
406
|
|
|
407
407
|
Storage:
|
|
@@ -876,7 +876,7 @@ Inside TUI:
|
|
|
876
876
|
|
|
877
877
|
```text
|
|
878
878
|
/bind-model openai-compatible Agentic --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
|
|
879
|
-
/ask xin
|
|
879
|
+
/ask xin chà o
|
|
880
880
|
```
|
|
881
881
|
|
|
882
882
|
Do not paste raw API keys into `/bind-model`. `api_key_env` means environment variable name, e.g. `OPENAI_API_KEY`.
|
|
@@ -908,133 +908,146 @@ If `/ask` shows connection refused, the configured OpenAI-compatible server is n
|
|
|
908
908
|
|
|
909
909
|
```text
|
|
910
910
|
/bind-model openai-compatible Agentic --role coder --base-url http://localhost:8000/v1 --api-key-env OPENAI_API_KEY
|
|
911
|
-
/ask xin
|
|
911
|
+
/ask xin chà o
|
|
912
912
|
```
|
|
913
913
|
|
|
914
914
|
Start your local server first, or change `--base-url` to the provider URL.
|
|
915
|
-
|
|
916
|
-
## Prompt Router Model
|
|
917
|
-
|
|
918
|
-
AgentRAM can use a separate small router model before normal prompts. The router classifies each prompt, then sends it to the right path.
|
|
919
|
-
|
|
920
|
-
```text
|
|
921
|
-
coding -> bound coding endpoints / Claude subagents
|
|
922
|
-
planning -> Goal Stack and drift check
|
|
923
|
-
memory -> AgentRAM note ingest
|
|
924
|
-
chat -> RAM retrieve only
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
Bind a router model:
|
|
928
|
-
|
|
929
|
-
```text
|
|
930
|
-
/bind-router openai-compatible qwen2.5:1.5b base_url=http://localhost:8000/v1 api_key_env=OPENAI_API_KEY
|
|
931
|
-
/route on
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
Then prompt without slash:
|
|
935
|
-
|
|
936
|
-
```text
|
|
937
|
-
fix adapter.py
|
|
938
|
-
|
|
939
|
-
ghi
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
Disable auto routing:
|
|
943
|
-
|
|
944
|
-
```text
|
|
945
|
-
/route off
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
## Supervisor + Small Agent Workflow
|
|
949
|
-
|
|
950
|
-
AgentRAM can run as a two-flow coding agent system, not only as a flat multi-model router.
|
|
951
|
-
|
|
952
|
-
```text
|
|
953
|
-
agentram
|
|
954
|
-
|
|
|
955
|
-
v
|
|
956
|
-
Parse User Request
|
|
957
|
-
|
|
|
958
|
-
v
|
|
959
|
-
Supervisor Memory
|
|
960
|
-
|
|
|
961
|
-
v
|
|
962
|
-
Task Classifier / Supervisor Plan
|
|
963
|
-
|----------------------|
|
|
964
|
-
v v
|
|
965
|
-
Main Agent
|
|
966
|
-
(
|
|
967
|
-
planning/coding/fix
|
|
968
|
-
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
915
|
+
|
|
916
|
+
## Prompt Router Model
|
|
917
|
+
|
|
918
|
+
AgentRAM can use a separate small router model before normal prompts. The router classifies each prompt, then sends it to the right path.
|
|
919
|
+
|
|
920
|
+
```text
|
|
921
|
+
coding -> bound coding endpoints / Claude subagents
|
|
922
|
+
planning -> Goal Stack and drift check
|
|
923
|
+
memory -> AgentRAM note ingest
|
|
924
|
+
chat -> RAM retrieve only
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
Bind a router model:
|
|
928
|
+
|
|
929
|
+
```text
|
|
930
|
+
/bind-router openai-compatible qwen2.5:1.5b base_url=http://localhost:8000/v1 api_key_env=OPENAI_API_KEY
|
|
931
|
+
/route on
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
Then prompt without slash:
|
|
935
|
+
|
|
936
|
+
```text
|
|
937
|
+
fix adapter.py
|
|
938
|
+
cap nhat goal stack
|
|
939
|
+
ghi nho dung Claude subagent
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Disable auto routing:
|
|
943
|
+
|
|
944
|
+
```text
|
|
945
|
+
/route off
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
## Supervisor + Small Agent Workflow
|
|
949
|
+
|
|
950
|
+
AgentRAM can run as a two-flow coding agent system, not only as a flat multi-model router.
|
|
951
|
+
|
|
952
|
+
```text
|
|
953
|
+
agentram
|
|
954
|
+
|
|
|
955
|
+
v
|
|
956
|
+
Parse User Request
|
|
957
|
+
|
|
|
958
|
+
v
|
|
959
|
+
Supervisor Memory
|
|
960
|
+
|
|
|
961
|
+
v
|
|
962
|
+
Task Classifier / Supervisor Plan
|
|
963
|
+
|----------------------|
|
|
964
|
+
v v
|
|
965
|
+
Main Agent
|
|
966
|
+
(Codex CLI / Claude Code / OpenAI-compatible)
|
|
967
|
+
planning/coding/fix, may edit files when bound to CLI provider
|
|
968
|
+
|
|
|
969
|
+
v
|
|
970
|
+
Small Agent
|
|
971
|
+
(Qwen/OpenAI-compatible/Anthropic)
|
|
972
|
+
notes/RAM/coding facts only
|
|
973
|
+
|
|
|
974
|
+
v
|
|
975
|
+
Merge + Update Memory
|
|
976
|
+
|
|
|
977
|
+
v
|
|
978
|
+
Return Result
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
Bind workflow agents:
|
|
982
|
+
|
|
983
|
+
```text
|
|
984
|
+
/bind-agent supervisor openai-compatible supervisor-model base_url=https://api.example.com/v1 api_key_env=SUPERVISOR_AGENT_KEY
|
|
985
|
+
/bind-agent main codex-cli current base_url="codex exec"
|
|
986
|
+
/bind-agent small openai-compatible qwen-codex base_url=http://localhost:8001/v1 api_key_env=SMALL_AGENT_KEY
|
|
987
|
+
/workflow
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Then use normal prompts without slash:
|
|
991
|
+
|
|
992
|
+
```text
|
|
993
|
+
refactor orchestration workflow
|
|
994
|
+
fix bug in adapter.py
|
|
995
|
+
plan next milestone
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
Runtime behavior:
|
|
999
|
+
|
|
1000
|
+
```text
|
|
1001
|
+
Supervisor creates plan -> Main CLI coding agent edits/returns result -> Small agent reads plan + main result and records RAM notes -> AgentRAM merges result
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1000
1004
|
Small agent should not own big direction. It records plans, decisions, files, risks, next tasks, and coding facts into RAM.
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1005
|
+
|
|
1006
|
+
CLI coding providers for file edits:
|
|
1007
|
+
|
|
1008
|
+
```text
|
|
1009
|
+
/bind-agent main codex-cli current base_url="codex exec"
|
|
1010
|
+
/bind-agent main claude-code claude base_url="claude"
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
Use `codex-cli` or `claude-code` only for the main agent when you want a coding agent subprocess that can inspect and edit files. Keep the small agent on an LLM-only provider so it records notes but does not edit code.
|
|
1014
|
+
|
|
1015
|
+
## Textual TUI
|
|
1016
|
+
|
|
1017
|
+
Install rich terminal UI with native scrollable chat pane:
|
|
1018
|
+
|
|
1019
|
+
```bash
|
|
1020
|
+
pip install agentram[tui]
|
|
1021
|
+
agentram tui
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1011
1024
|
When `textual` is installed, AgentRAM uses a real full-screen TUI with mouse wheel scrolling inside the chat frame. If `textual` is missing, it falls back to the basic print-based terminal UI.
|
|
1012
|
-
|
|
1013
|
-
## npm Python Dependencies
|
|
1014
|
-
|
|
1015
|
-
When installed through npm, AgentRAM runs a `postinstall` step that tries to install Python TUI dependencies for the Python interpreter used by `agentram`:
|
|
1016
|
-
|
|
1017
|
-
```text
|
|
1018
|
-
textual>=0.80
|
|
1019
|
-
prompt_toolkit>=3.0
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
Set target Python before install when needed:
|
|
1023
|
-
|
|
1024
|
-
```powershell
|
|
1025
|
-
$env:PYTHON="C:\Users\admin\anaconda3\python.exe"
|
|
1026
|
-
npm install -g agentram@latest
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
Skip automatic Python dependency install:
|
|
1030
|
-
|
|
1031
|
-
```powershell
|
|
1032
|
-
$env:AGENTRAM_SKIP_PY_DEPS="1"
|
|
1033
|
-
npm install -g agentram@latest
|
|
1034
|
-
```
|
|
1035
|
-
|
|
1036
|
-
If auto install fails, install manually:
|
|
1037
|
-
|
|
1038
|
-
```powershell
|
|
1039
|
-
python -m pip install textual prompt_toolkit
|
|
1025
|
+
|
|
1026
|
+
## npm Python Dependencies
|
|
1027
|
+
|
|
1028
|
+
When installed through npm, AgentRAM runs a `postinstall` step that tries to install Python TUI dependencies for the Python interpreter used by `agentram`:
|
|
1029
|
+
|
|
1030
|
+
```text
|
|
1031
|
+
textual>=0.80
|
|
1032
|
+
prompt_toolkit>=3.0
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
Set target Python before install when needed:
|
|
1036
|
+
|
|
1037
|
+
```powershell
|
|
1038
|
+
$env:PYTHON="C:\Users\admin\anaconda3\python.exe"
|
|
1039
|
+
npm install -g agentram@latest
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
Skip automatic Python dependency install:
|
|
1043
|
+
|
|
1044
|
+
```powershell
|
|
1045
|
+
$env:AGENTRAM_SKIP_PY_DEPS="1"
|
|
1046
|
+
npm install -g agentram@latest
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
If auto install fails, install manually:
|
|
1050
|
+
|
|
1051
|
+
```powershell
|
|
1052
|
+
python -m pip install textual prompt_toolkit
|
|
1040
1053
|
```
|
package/agentram/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import asyncio
|
|
@@ -101,6 +101,135 @@ SLASH_COMMANDS: list[tuple[str, str]] = [
|
|
|
101
101
|
]
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
|
|
105
|
+
CLAUDE_COMPAT_COMMANDS: list[tuple[str, str]] = [
|
|
106
|
+
("/add-dir <path>", "Claude: add a working directory for file access"),
|
|
107
|
+
("/advisor [model|off]", "Claude: enable or disable advisor model"),
|
|
108
|
+
("/autofix-pr [prompt]", "Claude: cloud session fixes PR CI/review comments"),
|
|
109
|
+
("/background [prompt]", "Claude: detach session as background agent"),
|
|
110
|
+
("/bg [prompt]", "Claude alias: /background"),
|
|
111
|
+
("/batch <instruction>", "Claude workflow: parallel codebase change"),
|
|
112
|
+
("/branch [name]", "Claude: fork conversation branch"),
|
|
113
|
+
("/btw <question>", "Claude: side question without history"),
|
|
114
|
+
("/cd <path>", "Claude: move session to another directory"),
|
|
115
|
+
("/chrome", "Claude: configure Claude in Chrome"),
|
|
116
|
+
("/claude-api [migrate|managed-agents-onboard]", "Claude skill: API reference and migration"),
|
|
117
|
+
("/reset [name]", "Claude alias: /clear"),
|
|
118
|
+
("/new [name]", "Claude alias: /clear"),
|
|
119
|
+
("/code-review [level] [--fix] [--comment] [target]", "Claude skill: review current diff"),
|
|
120
|
+
("/color [color|default]", "Claude: prompt bar color"),
|
|
121
|
+
("/compact [instructions]", "Claude: summarize context to free space"),
|
|
122
|
+
("/config [key=value ...]", "Claude: settings interface"),
|
|
123
|
+
("/settings [key=value ...]", "Claude alias: /config"),
|
|
124
|
+
("/context [all]", "Claude: visualize context usage"),
|
|
125
|
+
("/copy [N]", "Claude: copy assistant response"),
|
|
126
|
+
("/cost", "Claude alias: /usage"),
|
|
127
|
+
("/dataviz [request]", "Claude skill: chart/dashboard design guidance"),
|
|
128
|
+
("/debug [description]", "Claude skill: enable and inspect debug logs"),
|
|
129
|
+
("/deep-research <question>", "Claude workflow: web research report"),
|
|
130
|
+
("/design-login", "Claude: authorize design-system access"),
|
|
131
|
+
("/design-sync [hint]", "Claude skill: sync React design system"),
|
|
132
|
+
("/desktop", "Claude: continue in desktop app"),
|
|
133
|
+
("/app", "Claude alias: /desktop"),
|
|
134
|
+
("/diff", "Claude: interactive diff viewer"),
|
|
135
|
+
("/doctor", "Claude: diagnose install/runtime"),
|
|
136
|
+
("/effort [level|auto]", "Claude: set reasoning effort"),
|
|
137
|
+
("/export [filename]", "Claude: export conversation"),
|
|
138
|
+
("/fast [on|off]", "Claude: toggle fast mode"),
|
|
139
|
+
("/feedback [report]", "Claude: send feedback"),
|
|
140
|
+
("/bug [report]", "Claude alias: /feedback"),
|
|
141
|
+
("/share [report]", "Claude alias: /feedback"),
|
|
142
|
+
("/fewer-permission-prompts", "Claude skill: reduce permission prompts"),
|
|
143
|
+
("/focus", "Claude: toggle focus view"),
|
|
144
|
+
("/fork <directive>", "Claude: forked background subagent"),
|
|
145
|
+
("/heapdump", "Claude: write heap snapshot"),
|
|
146
|
+
("/hooks", "Claude: view hook config"),
|
|
147
|
+
("/ide", "Claude: manage IDE integration"),
|
|
148
|
+
("/insights", "Claude: analyze Claude Code sessions"),
|
|
149
|
+
("/install-github-app", "Claude: install GitHub app"),
|
|
150
|
+
("/install-slack-app", "Claude: install Slack app"),
|
|
151
|
+
("/keybindings", "Claude: open keyboard shortcut file"),
|
|
152
|
+
("/login", "Claude: sign in"),
|
|
153
|
+
("/logout", "Claude: sign out"),
|
|
154
|
+
("/loop [interval] [prompt]", "Claude skill: repeated prompt loop"),
|
|
155
|
+
("/proactive [interval] [prompt]", "Claude alias: /loop"),
|
|
156
|
+
("/mcp [reconnect|enable|disable]", "Claude: manage MCP servers"),
|
|
157
|
+
("/memory", "Claude: edit CLAUDE.md memory"),
|
|
158
|
+
("/mobile", "Claude: show mobile app QR"),
|
|
159
|
+
("/ios", "Claude alias: /mobile"),
|
|
160
|
+
("/android", "Claude alias: /mobile"),
|
|
161
|
+
("/model [model]", "Claude: switch model"),
|
|
162
|
+
("/passes", "Claude: share Claude Code pass"),
|
|
163
|
+
("/permissions", "Claude: manage tool permissions"),
|
|
164
|
+
("/allowed-tools", "Claude alias: /permissions"),
|
|
165
|
+
("/plan [description]", "Claude: enter plan mode"),
|
|
166
|
+
("/plugin [subcommand]", "Claude: manage plugins"),
|
|
167
|
+
("/powerup", "Claude: feature lessons"),
|
|
168
|
+
("/pr-comments [PR]", "Claude legacy: PR comments"),
|
|
169
|
+
("/privacy-settings", "Claude: privacy settings"),
|
|
170
|
+
("/radio", "Claude: Claude FM radio"),
|
|
171
|
+
("/recap", "Claude: one-line session summary"),
|
|
172
|
+
("/release-notes", "Claude: changelog picker"),
|
|
173
|
+
("/reload-plugins [--force]", "Claude: reload plugins"),
|
|
174
|
+
("/reload-skills", "Claude: reload skills and commands"),
|
|
175
|
+
("/remote-control", "Claude: remote control from claude.ai"),
|
|
176
|
+
("/rc", "Claude alias: /remote-control"),
|
|
177
|
+
("/remote-env", "Claude: cloud agent environment"),
|
|
178
|
+
("/rename [name]", "Claude: rename session"),
|
|
179
|
+
("/resume [session]", "Claude: resume conversation"),
|
|
180
|
+
("/continue [session]", "Claude alias: /resume"),
|
|
181
|
+
("/review [PR]", "Claude: review GitHub PR"),
|
|
182
|
+
("/rewind", "Claude: rewind conversation/code"),
|
|
183
|
+
("/checkpoint", "Claude alias: /rewind"),
|
|
184
|
+
("/undo", "Claude alias: /rewind"),
|
|
185
|
+
("/run", "Claude skill: run app and verify change"),
|
|
186
|
+
("/run-skill-generator", "Claude skill: generate run/verify skill"),
|
|
187
|
+
("/sandbox", "Claude: toggle sandbox mode"),
|
|
188
|
+
("/schedule [description]", "Claude: cloud routine"),
|
|
189
|
+
("/routines [description]", "Claude alias: /schedule"),
|
|
190
|
+
("/scroll-speed", "Claude: adjust mouse wheel speed"),
|
|
191
|
+
("/security-review", "Claude: security review current diff"),
|
|
192
|
+
("/setup-bedrock", "Claude: Bedrock setup wizard"),
|
|
193
|
+
("/setup-vertex", "Claude: Vertex setup wizard"),
|
|
194
|
+
("/simplify [target]", "Claude skill: cleanup review and fixes"),
|
|
195
|
+
("/skills", "Claude: list skills"),
|
|
196
|
+
("/stats", "Claude alias: /usage"),
|
|
197
|
+
("/statusline", "Claude: configure status line"),
|
|
198
|
+
("/stickers", "Claude: order stickers"),
|
|
199
|
+
("/stop", "Claude: stop background session"),
|
|
200
|
+
("/tasks", "Claude: view background work"),
|
|
201
|
+
("/bashes", "Claude alias: /tasks"),
|
|
202
|
+
("/team-onboarding", "Claude: generate team onboarding"),
|
|
203
|
+
("/teleport", "Claude: pull web session into terminal"),
|
|
204
|
+
("/tp", "Claude alias: /teleport"),
|
|
205
|
+
("/terminal-setup", "Claude: terminal keybindings setup"),
|
|
206
|
+
("/theme", "Claude: change color theme"),
|
|
207
|
+
("/tui [default|fullscreen]", "Claude: switch renderer"),
|
|
208
|
+
("/ultraplan <prompt>", "Claude: draft cloud plan"),
|
|
209
|
+
("/ultrareview [PR]", "Claude: deep cloud review"),
|
|
210
|
+
("/upgrade", "Claude: open upgrade page"),
|
|
211
|
+
("/usage", "Claude: show usage and cost"),
|
|
212
|
+
("/usage-credits", "Claude: configure usage credits"),
|
|
213
|
+
("/verify", "Claude skill: verify app behavior"),
|
|
214
|
+
("/vim", "Claude legacy: use /config for editor mode"),
|
|
215
|
+
("/voice [hold|tap|off]", "Claude: voice dictation"),
|
|
216
|
+
("/web-setup", "Claude: connect GitHub for web"),
|
|
217
|
+
("/workflows", "Claude: workflow progress view"),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
CLAUDE_COMPAT_NAMES = {command.split()[0] for command, _ in CLAUDE_COMPAT_COMMANDS}
|
|
221
|
+
|
|
222
|
+
def slash_entries() -> list[tuple[str, str]]:
|
|
223
|
+
seen: set[str] = set()
|
|
224
|
+
entries: list[tuple[str, str]] = []
|
|
225
|
+
for command, description in [*SLASH_COMMANDS, *CLAUDE_COMPAT_COMMANDS]:
|
|
226
|
+
name = command.split()[0]
|
|
227
|
+
if name in seen:
|
|
228
|
+
continue
|
|
229
|
+
seen.add(name)
|
|
230
|
+
entries.append((command, description))
|
|
231
|
+
return entries
|
|
232
|
+
|
|
104
233
|
SLASH_HELP = """Slash commands:
|
|
105
234
|
/help Show commands
|
|
106
235
|
/init Create .agentram files
|
|
@@ -353,6 +482,8 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
353
482
|
self.prompt_history: list[str] = []
|
|
354
483
|
self.history_index: int | None = None
|
|
355
484
|
self.last_ctrl_c_at = 0.0
|
|
485
|
+
self.setup_slots = ("supervisor", "main", "small")
|
|
486
|
+
self.setup_index = 0
|
|
356
487
|
|
|
357
488
|
def compose(self) -> ComposeResult:
|
|
358
489
|
yield Header(show_clock=True)
|
|
@@ -360,18 +491,20 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
360
491
|
yield Static(id="sidebar")
|
|
361
492
|
with Vertical(id="main"):
|
|
362
493
|
with Vertical(id="setup"):
|
|
363
|
-
yield Static("Model setup -
|
|
494
|
+
yield Static("Model setup - 1/3 Supervisor", id="setup-title")
|
|
364
495
|
for slot, label, default_model in [
|
|
365
|
-
("supervisor", "Supervisor", ""),
|
|
366
|
-
("main", "Main coding", ""),
|
|
367
|
-
("small", "Small memory", ""),
|
|
496
|
+
("supervisor", "1. Supervisor", ""),
|
|
497
|
+
("main", "2. Main coding", ""),
|
|
498
|
+
("small", "3. Small memory", ""),
|
|
368
499
|
]:
|
|
369
|
-
with Vertical(classes="agent-setup"):
|
|
500
|
+
with Vertical(id=f"setup-{slot}-panel", classes="agent-setup"):
|
|
370
501
|
yield Static(f"{label} agent")
|
|
371
502
|
yield Select(
|
|
372
503
|
[
|
|
373
504
|
("OpenAI Compatible", "openai-compatible"),
|
|
374
505
|
("Anthropic Compatible", "anthropic"),
|
|
506
|
+
("Claude Code CLI", "claude-code"),
|
|
507
|
+
("Codex CLI", "codex-cli"),
|
|
375
508
|
("Custom", "custom"),
|
|
376
509
|
],
|
|
377
510
|
value="openai-compatible",
|
|
@@ -380,14 +513,16 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
380
513
|
)
|
|
381
514
|
yield Input(placeholder="Custom provider if preset=Custom", id=f"setup-{slot}-custom-provider", classes="setup-input")
|
|
382
515
|
yield Input(value=default_model, placeholder="Model name", id=f"setup-{slot}-model", classes="setup-input")
|
|
383
|
-
yield Input(placeholder="Base URL, e.g. http://localhost:20128/v1", id=f"setup-{slot}-base-url", classes="setup-input")
|
|
516
|
+
yield Input(placeholder="Base URL or CLI command, e.g. http://localhost:20128/v1 / claude / codex exec", id=f"setup-{slot}-base-url", classes="setup-input")
|
|
384
517
|
yield Input(value="OPENAI_API_KEY", placeholder="API key or env name", id=f"setup-{slot}-api-key-env", classes="setup-input")
|
|
385
518
|
with Horizontal(id="setup-actions"):
|
|
386
|
-
yield Button("
|
|
519
|
+
yield Button("Back", id="setup-prev")
|
|
520
|
+
yield Button("Next", id="setup-next", variant="primary")
|
|
521
|
+
yield Button("Save setup", id="setup-save", variant="success")
|
|
387
522
|
yield Button("Skip", id="setup-skip")
|
|
388
523
|
yield RichLog(id="chat", wrap=True, highlight=True, markup=False, auto_scroll=True)
|
|
389
524
|
yield Static(id="palette")
|
|
390
|
-
yield Input(placeholder="Prompt
|
|
525
|
+
yield Input(placeholder="Prompt or /command. Mouse wheel scroll in chat.", id="input")
|
|
391
526
|
yield Footer()
|
|
392
527
|
|
|
393
528
|
def on_mount(self) -> None:
|
|
@@ -395,10 +530,11 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
395
530
|
self.chat.write("system > AgentRAM Textual TUI ready")
|
|
396
531
|
self.chat.write(f"system > RAM root: {self.context.ram_root}")
|
|
397
532
|
self.prefill_setup_from_profile()
|
|
533
|
+
self.refresh_setup_step()
|
|
398
534
|
self.refresh_sidebar()
|
|
399
535
|
self.set_interval(1.0, self.refresh_sidebar)
|
|
400
536
|
try:
|
|
401
|
-
self.
|
|
537
|
+
self.focus_current_setup_model()
|
|
402
538
|
except Exception: # noqa: BLE001 - fallback if setup is hidden/unmounted
|
|
403
539
|
self.query_one("#input", Input).focus()
|
|
404
540
|
|
|
@@ -418,7 +554,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
418
554
|
if not endpoint:
|
|
419
555
|
continue
|
|
420
556
|
provider = endpoint.provider
|
|
421
|
-
preset = provider if provider in {"openai-compatible", "anthropic"} else "custom"
|
|
557
|
+
preset = provider if provider in {"openai-compatible", "anthropic", "claude-code", "codex-cli"} else "custom"
|
|
422
558
|
self.query_one(f"#setup-{slot}-provider", Select).value = preset
|
|
423
559
|
self.query_one(f"#setup-{slot}-custom-provider", Input).value = "" if preset != "custom" else provider
|
|
424
560
|
self.query_one(f"#setup-{slot}-model", Input).value = endpoint.model
|
|
@@ -426,6 +562,37 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
426
562
|
self.query_one(f"#setup-{slot}-api-key-env", Input).value = endpoint.api_key_env
|
|
427
563
|
self.chat.write("system > Existing workflow models loaded. Save to update, or Skip to keep.")
|
|
428
564
|
|
|
565
|
+
def current_setup_slot(self) -> str:
|
|
566
|
+
return self.setup_slots[self.setup_index]
|
|
567
|
+
|
|
568
|
+
def current_setup_label(self) -> str:
|
|
569
|
+
labels = {"supervisor": "Supervisor", "main": "Main coding", "small": "Small memory"}
|
|
570
|
+
return labels[self.current_setup_slot()]
|
|
571
|
+
|
|
572
|
+
def refresh_setup_step(self) -> None:
|
|
573
|
+
total = len(self.setup_slots)
|
|
574
|
+
slot = self.current_setup_slot()
|
|
575
|
+
self.query_one("#setup-title", Static).update(f"Model setup - {self.setup_index + 1}/{total} {self.current_setup_label()}")
|
|
576
|
+
for item in self.setup_slots:
|
|
577
|
+
self.query_one(f"#setup-{item}-panel", Vertical).styles.display = "block" if item == slot else "none"
|
|
578
|
+
|
|
579
|
+
def focus_current_setup_model(self) -> None:
|
|
580
|
+
self.query_one(f"#setup-{self.current_setup_slot()}-model", Input).focus()
|
|
581
|
+
|
|
582
|
+
def action_next_setup(self) -> None:
|
|
583
|
+
if self.setup_index < len(self.setup_slots) - 1:
|
|
584
|
+
self.setup_index += 1
|
|
585
|
+
self.refresh_setup_step()
|
|
586
|
+
self.focus_current_setup_model()
|
|
587
|
+
else:
|
|
588
|
+
self.action_save_setup()
|
|
589
|
+
|
|
590
|
+
def action_prev_setup(self) -> None:
|
|
591
|
+
if self.setup_index > 0:
|
|
592
|
+
self.setup_index -= 1
|
|
593
|
+
self.refresh_setup_step()
|
|
594
|
+
self.focus_current_setup_model()
|
|
595
|
+
|
|
429
596
|
def setup_value(self, widget_id: str) -> str:
|
|
430
597
|
return self.query_one(f"#{widget_id}", Input).value.strip()
|
|
431
598
|
|
|
@@ -473,8 +640,10 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
473
640
|
|
|
474
641
|
def show_setup(self) -> None:
|
|
475
642
|
self.setup_done = False
|
|
643
|
+
self.setup_index = 0
|
|
476
644
|
self.query_one("#setup", Vertical).styles.display = "block"
|
|
477
|
-
self.
|
|
645
|
+
self.refresh_setup_step()
|
|
646
|
+
self.focus_current_setup_model()
|
|
478
647
|
|
|
479
648
|
def action_history_prev(self) -> None:
|
|
480
649
|
input_widget = self.query_one("#input", Input)
|
|
@@ -515,6 +684,10 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
515
684
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
516
685
|
if event.button.id == "setup-save":
|
|
517
686
|
self.action_save_setup()
|
|
687
|
+
elif event.button.id == "setup-next":
|
|
688
|
+
self.action_next_setup()
|
|
689
|
+
elif event.button.id == "setup-prev":
|
|
690
|
+
self.action_prev_setup()
|
|
518
691
|
elif event.button.id == "setup-skip":
|
|
519
692
|
self.chat.write("system > Model setup skipped. Use /bind-agent later.")
|
|
520
693
|
self.hide_setup()
|
|
@@ -535,7 +708,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
535
708
|
return
|
|
536
709
|
query = value.strip().lower().lstrip("/")
|
|
537
710
|
matches = []
|
|
538
|
-
for command, description in
|
|
711
|
+
for command, description in slash_entries():
|
|
539
712
|
if not command.startswith("/"):
|
|
540
713
|
continue
|
|
541
714
|
command_name = command.split()[0]
|
|
@@ -547,7 +720,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
547
720
|
if not matches:
|
|
548
721
|
palette.update("No matching slash command")
|
|
549
722
|
else:
|
|
550
|
-
lines = ["Slash commands
|
|
723
|
+
lines = ["Slash commands - Tab completes first match"]
|
|
551
724
|
lines.extend(f" {command.ljust(18)} {description}" for command, description in matches[:8])
|
|
552
725
|
palette.update("\n".join(lines))
|
|
553
726
|
palette.styles.display = "block"
|
|
@@ -571,7 +744,7 @@ def run_textual_tui(context: McpContext) -> bool:
|
|
|
571
744
|
|
|
572
745
|
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
573
746
|
if event.input.id and str(event.input.id).startswith("setup-"):
|
|
574
|
-
self.
|
|
747
|
+
self.action_next_setup()
|
|
575
748
|
return
|
|
576
749
|
text = event.value.strip()
|
|
577
750
|
event.input.value = ""
|
|
@@ -631,11 +804,11 @@ def run_with_activity(context: McpContext, messages: list[str], user_input: str)
|
|
|
631
804
|
|
|
632
805
|
|
|
633
806
|
def slash_command_names() -> list[str]:
|
|
634
|
-
return [command.split()[0] for command, _ in
|
|
807
|
+
return [command.split()[0] for command, _ in slash_entries() if command.startswith("/")]
|
|
635
808
|
|
|
636
809
|
|
|
637
810
|
def slash_command_meta() -> dict[str, str]:
|
|
638
|
-
return {command.split()[0]: description for command, description in
|
|
811
|
+
return {command.split()[0]: description for command, description in slash_entries() if command.startswith("/")}
|
|
639
812
|
|
|
640
813
|
|
|
641
814
|
def create_prompt_session() -> Any | None:
|
|
@@ -655,7 +828,7 @@ def create_prompt_session() -> Any | None:
|
|
|
655
828
|
completer=completer,
|
|
656
829
|
complete_while_typing=True,
|
|
657
830
|
reserve_space_for_menu=12,
|
|
658
|
-
bottom_toolbar="Type / for slash commands
|
|
831
|
+
bottom_toolbar="Type / for slash commands | /agents for subagents | /ask to call bound models",
|
|
659
832
|
)
|
|
660
833
|
|
|
661
834
|
|
|
@@ -755,9 +928,25 @@ def handle_slash_command(context: McpContext, text: str, messages: list[str] | N
|
|
|
755
928
|
return note_text(context, raw_rest)
|
|
756
929
|
if command == "/clear":
|
|
757
930
|
return "__clear__"
|
|
931
|
+
if command in CLAUDE_COMPAT_NAMES:
|
|
932
|
+
return claude_compat_command_text(command, raw_rest)
|
|
758
933
|
return f"Unknown slash command: {command}\n\n{SLASH_HELP}"
|
|
759
934
|
|
|
760
935
|
|
|
936
|
+
def claude_compat_command_text(command: str, arguments: str) -> str:
|
|
937
|
+
descriptions = slash_command_meta()
|
|
938
|
+
description = descriptions.get(command, "Claude Code command")
|
|
939
|
+
return "\n".join(
|
|
940
|
+
[
|
|
941
|
+
f"Claude-compatible command: {command}",
|
|
942
|
+
f"Purpose: {description}",
|
|
943
|
+
f"Args: {arguments or '-'}",
|
|
944
|
+
"Status: listed for Claude parity in AgentRAM TUI.",
|
|
945
|
+
"Native execution is not implemented here unless AgentRAM has its own handler for this command.",
|
|
946
|
+
"Run the same command inside Claude Code for Claude-native behavior.",
|
|
947
|
+
]
|
|
948
|
+
)
|
|
949
|
+
|
|
761
950
|
def init_text(context: McpContext) -> str:
|
|
762
951
|
data = payload(call_tool(context, "agentram_init", {}))
|
|
763
952
|
created = data.get("created", [])
|
|
@@ -852,7 +1041,7 @@ def agents_text() -> str:
|
|
|
852
1041
|
def slash_palette_text(filter_text: str = "") -> str:
|
|
853
1042
|
normalized = filter_text.strip().lower().lstrip("/")
|
|
854
1043
|
rows = []
|
|
855
|
-
for command, description in
|
|
1044
|
+
for command, description in slash_entries():
|
|
856
1045
|
command_key = command.lower().lstrip("/")
|
|
857
1046
|
if normalized and normalized not in command_key:
|
|
858
1047
|
continue
|
|
@@ -993,45 +1182,154 @@ def workflow_text(context: McpContext) -> str:
|
|
|
993
1182
|
lines.append(f"- {slot}: not bound")
|
|
994
1183
|
return "\n".join(lines)
|
|
995
1184
|
|
|
996
|
-
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
997
|
-
agents = context.profile_store.list_agents()
|
|
998
|
-
supervisor = agents.get("supervisor")
|
|
999
|
-
main_agent = agents.get("main")
|
|
1000
|
-
small_agent = agents.get("small")
|
|
1001
|
-
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1185
|
+
def supervisor_workflow_text(context: McpContext, prompt: str) -> str:
|
|
1186
|
+
agents = context.profile_store.list_agents()
|
|
1187
|
+
supervisor = agents.get("supervisor")
|
|
1188
|
+
main_agent = agents.get("main")
|
|
1189
|
+
small_agent = agents.get("small")
|
|
1190
|
+
memory_context = retrieve_text(context, prompt, task_id="codex", limit=8)
|
|
1191
|
+
route = classify_agent_runtime_route(context, prompt)
|
|
1192
|
+
intent = str(route.get("intent", "chat"))
|
|
1193
|
+
outputs: dict[str, str] = {}
|
|
1194
|
+
|
|
1195
|
+
if intent == "planning":
|
|
1196
|
+
plan = supervisor_plan(supervisor, prompt, memory_context)
|
|
1197
|
+
outputs["supervisor"] = plan
|
|
1198
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
|
|
1199
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1200
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1201
|
+
return "\n".join(
|
|
1202
|
+
[
|
|
1203
|
+
f"Workflow: route=planning confidence={float(route.get('confidence', 0.0)):.2f} -> supervisor -> small memory note -> merge",
|
|
1204
|
+
"Supervisor plan:",
|
|
1205
|
+
plan,
|
|
1206
|
+
"",
|
|
1207
|
+
"Small memory agent notes:",
|
|
1208
|
+
outputs.get("small", "No small agent bound."),
|
|
1209
|
+
]
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
if intent == "memory":
|
|
1213
|
+
plan = "Memory-only event. Small agent writes RAM note directly."
|
|
1214
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "memory_event", prompt, plan, memory_context)
|
|
1215
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1216
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1217
|
+
return "\n".join(
|
|
1218
|
+
[
|
|
1219
|
+
f"Workflow: route=memory confidence={float(route.get('confidence', 0.0)):.2f} -> small memory note -> merge",
|
|
1220
|
+
"Small memory agent notes:",
|
|
1221
|
+
outputs.get("small", "No small agent bound."),
|
|
1222
|
+
]
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
plan = runtime_execution_plan(context, supervisor, prompt, memory_context, route)
|
|
1226
|
+
outputs["supervisor"] = plan
|
|
1227
|
+
outputs["small_after_plan"] = small_agent_observe(context, small_agent, prompt, "supervisor_plan", plan, plan, memory_context)
|
|
1228
|
+
if main_agent:
|
|
1229
|
+
try:
|
|
1230
|
+
outputs["main"] = call_agent_endpoint(main_agent, build_main_agent_prompt(prompt, plan, memory_context))
|
|
1231
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
1232
|
+
outputs["main"] = f"error: {model_error_hint(str(error))}"
|
|
1233
|
+
else:
|
|
1234
|
+
outputs["main"] = "No main agent bound. Use /bind-agent main <provider> <model>."
|
|
1235
|
+
outputs["small"] = small_agent_observe(context, small_agent, prompt, "main_result", outputs.get("main", ""), plan, memory_context)
|
|
1236
|
+
sync_goal_state_from_small_agent(context, prompt, plan, outputs.get("small", ""))
|
|
1237
|
+
persist_workflow_memory(context, prompt, plan, outputs)
|
|
1238
|
+
return "\n".join(
|
|
1239
|
+
[
|
|
1240
|
+
f"Workflow: route={intent} confidence={float(route.get('confidence', 0.0)):.2f} -> main -> small memory note -> merge",
|
|
1241
|
+
"Supervisor plan:",
|
|
1242
|
+
plan,
|
|
1243
|
+
"",
|
|
1244
|
+
"Main agent result:",
|
|
1245
|
+
outputs.get("main", "-"),
|
|
1246
|
+
"",
|
|
1247
|
+
"Small memory agent after plan:",
|
|
1248
|
+
outputs.get("small_after_plan", "No small agent bound."),
|
|
1249
|
+
"",
|
|
1250
|
+
"Small memory agent notes:",
|
|
1251
|
+
outputs.get("small", "No small agent bound."),
|
|
1252
|
+
]
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
def classify_agent_runtime_route(context: McpContext, prompt: str) -> dict[str, object]:
|
|
1256
|
+
lower = prompt.lower()
|
|
1257
|
+
execute_words = [
|
|
1258
|
+
"bắt đầu làm", "bat dau lam", "làm đi", "lam di", "tiến hành", "tien hanh", "thực hiện", "thuc hien",
|
|
1259
|
+
"chốt kế hoạch", "chot ke hoach", "bắt tay", "start", "execute", "implement", "do it", "go ahead",
|
|
1260
|
+
]
|
|
1261
|
+
plan_words = ["lên kế hoạch", "len ke hoach", "kế hoạch", "ke hoach", "plan", "roadmap", "milestone", "thiết kế", "thiet ke"]
|
|
1262
|
+
if any(word in lower for word in execute_words):
|
|
1263
|
+
return {"intent": "execution", "confidence": 0.9, "source": "heuristic"}
|
|
1264
|
+
if any(word in lower for word in plan_words):
|
|
1265
|
+
return {"intent": "planning", "confidence": 0.85, "source": "heuristic"}
|
|
1266
|
+
decision = classify_prompt_intent(context, prompt)
|
|
1267
|
+
intent = str(decision.get("intent", "chat"))
|
|
1268
|
+
if intent == "coding":
|
|
1269
|
+
decision["intent"] = "execution"
|
|
1270
|
+
return decision
|
|
1271
|
+
|
|
1272
|
+
def runtime_execution_plan(context: McpContext, supervisor: AgentModelEndpoint | None, prompt: str, memory_context: str, route: dict[str, object]) -> str:
|
|
1273
|
+
state = context.goal_store.load().context_block()
|
|
1274
|
+
if state:
|
|
1275
|
+
return "\n".join(["Use current accepted Goal Stack as execution plan.", state])
|
|
1276
|
+
if str(route.get("intent")) == "execution" and any(word in prompt.lower() for word in ["bắt đầu", "bat dau", "làm đi", "lam di", "execute", "do it"]):
|
|
1277
|
+
return "User approved previous plan. Use AgentRAM memory context and current prompt to execute next concrete task."
|
|
1278
|
+
return supervisor_plan(supervisor, prompt, memory_context)
|
|
1279
|
+
|
|
1280
|
+
def small_agent_observe(context: McpContext, small_agent: AgentModelEndpoint | None, prompt: str, event_type: str, event_content: str, plan: str, memory_context: str) -> str:
|
|
1281
|
+
if not small_agent:
|
|
1282
|
+
return "No small agent bound."
|
|
1283
|
+
try:
|
|
1284
|
+
return call_agent_endpoint(
|
|
1285
|
+
small_agent,
|
|
1286
|
+
build_small_agent_prompt(prompt, plan, memory_context, event_content, event_type=event_type),
|
|
1287
|
+
)
|
|
1288
|
+
except Exception as error: # noqa: BLE001 - agent boundary
|
|
1289
|
+
return f"error: {model_error_hint(str(error))}"
|
|
1290
|
+
|
|
1291
|
+
def sync_goal_state_from_small_agent(context: McpContext, prompt: str, plan: str, small_notes: str) -> None:
|
|
1292
|
+
if not small_notes.strip() or small_notes.strip().startswith("error:"):
|
|
1293
|
+
return
|
|
1294
|
+
state = context.goal_store.load()
|
|
1295
|
+
goal = extract_labeled_value(small_notes, ("current_goal", "goal", "mục tiêu", "muc tieu")) or first_nonempty_line(plan)
|
|
1296
|
+
task = extract_labeled_value(small_notes, ("current_task", "task", "nhiệm vụ", "nhiem vu")) or first_nonempty_line(small_notes) or prompt
|
|
1297
|
+
patch: dict[str, object] = {
|
|
1298
|
+
"current_task": trim(task.strip(), 180),
|
|
1299
|
+
"goal_history": [trim(goal.strip() or prompt.strip(), 180)],
|
|
1300
|
+
}
|
|
1301
|
+
if not state.mission:
|
|
1302
|
+
patch["mission"] = "Xây dựng và vận hành AgentRAM cho coding agents."
|
|
1303
|
+
if not state.project_goal:
|
|
1304
|
+
patch["project_goal"] = "AgentRAM workflow: supervisor lập kế hoạch, main coding agent thực thi, small memory agent ghi RAM."
|
|
1305
|
+
if goal:
|
|
1306
|
+
patch["current_goal"] = trim(goal.strip(), 180)
|
|
1307
|
+
context.goal_store.update(patch)
|
|
1308
|
+
|
|
1309
|
+
def extract_labeled_value(text: str, labels: tuple[str, ...]) -> str:
|
|
1310
|
+
for line in text.splitlines():
|
|
1311
|
+
cleaned = line.strip().lstrip("-*").strip()
|
|
1312
|
+
for label in labels:
|
|
1313
|
+
pattern = rf"^{re.escape(label)}\s*[:=]\s*(.+)$"
|
|
1314
|
+
match = re.match(pattern, cleaned, flags=re.IGNORECASE)
|
|
1315
|
+
if match:
|
|
1316
|
+
return match.group(1).strip().strip("`*_-")
|
|
1317
|
+
return ""
|
|
1318
|
+
|
|
1319
|
+
def first_nonempty_line(value: str) -> str:
|
|
1320
|
+
for line in value.splitlines():
|
|
1321
|
+
cleaned = line.strip().lstrip("-0123456789. )")
|
|
1322
|
+
if cleaned and not cleaned.lower().startswith(("supervisor error", "fallback plan")):
|
|
1323
|
+
return cleaned
|
|
1324
|
+
return ""
|
|
1325
|
+
|
|
1326
|
+
def persist_workflow_memory(context: McpContext, prompt: str, plan: str, outputs: dict[str, str]) -> None:
|
|
1327
|
+
small_notes = "\n".join(
|
|
1328
|
+
value.strip()
|
|
1329
|
+
for key, value in outputs.items()
|
|
1330
|
+
if key.startswith("small") and value.strip() and not value.strip().startswith("error:")
|
|
1331
|
+
)
|
|
1332
|
+
main_result = outputs.get("main", "").strip()
|
|
1035
1333
|
note_parts = [
|
|
1036
1334
|
f"User request: {prompt}",
|
|
1037
1335
|
f"Supervisor plan: {trim(plan.strip(), 1200)}" if plan.strip() else "Supervisor plan: -",
|
|
@@ -1062,14 +1360,14 @@ def supervisor_plan(supervisor: AgentModelEndpoint | None, prompt: str, memory_c
|
|
|
1062
1360
|
def call_agent_endpoint(endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
1063
1361
|
return MultiModelCodingOrchestrator([endpoint])._call_endpoint(endpoint, prompt)
|
|
1064
1362
|
|
|
1065
|
-
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
1066
|
-
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Create concise plan
|
|
1067
|
-
|
|
1068
|
-
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1069
|
-
return "\n".join(["You are AgentRAM main coding agent.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
1070
|
-
|
|
1071
|
-
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1072
|
-
return "\n".join(["You are AgentRAM small memory agent.", "Do not edit code.
|
|
1363
|
+
def build_supervisor_prompt(prompt: str, memory_context: str) -> str:
|
|
1364
|
+
return "\n".join(["You are AgentRAM supervisor. Act like a senior coding-agent boss.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Create a concise plan only. Do not code. Main agent runs later only after user approves/starts execution.", "Small memory agent observes this plan and writes RAM notes.", "Return only actionable plan. No markdown table.", "User request:", prompt, "AgentRAM context:", memory_context])
|
|
1365
|
+
|
|
1366
|
+
def build_main_agent_prompt(prompt: str, plan: str, memory_context: str) -> str:
|
|
1367
|
+
return "\n".join(["You are AgentRAM main coding agent.", "Nếu user prompt bằng tiếng Việt, trả lời bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "Follow supervisor plan. Focus on planning, orchestration, coding, refactor, bug fix, and final result.", "Do not invent repo facts. Mention files to inspect/change.", "User request:", prompt, "Supervisor plan:", plan, "AgentRAM context:", memory_context])
|
|
1368
|
+
|
|
1369
|
+
def build_small_agent_prompt(prompt: str, plan: str, memory_context: str, event_content: str = "", event_type: str = "main_result") -> str:
|
|
1370
|
+
return "\n".join(["You are AgentRAM small memory agent.", "Nếu user prompt bằng tiếng Việt, ghi note RAM bằng tiếng Việt. Giữ code, command, path, errors nguyên văn.", "You participate in every AgentRAM event directly: user request, supervisor plan, main result, compact/merge.", "Do not edit code. Observe event content, then note decisions, risks, files, next tasks, and coding facts for RAM.", "You are responsible for setting the visible RAM goal/task from the latest event.", "Return concise memory notes only. Không dùng tiếng Anh nếu user đang dùng tiếng Việt.", "Bắt buộc gồm 2 dòng đầu theo format: current_goal: ... và current_task: ...", "User request:", prompt, "Supervisor plan:", plan, "Observed event type:", event_type, "Observed event content:", event_content or "-", "AgentRAM context:", memory_context])
|
|
1073
1371
|
|
|
1074
1372
|
def bind_router_text(context: McpContext, options: dict[str, object]) -> str:
|
|
1075
1373
|
api_key_env = str(options.get("api_key_env", ""))
|
|
@@ -1154,9 +1452,9 @@ def extract_json_object(text: str) -> str:
|
|
|
1154
1452
|
|
|
1155
1453
|
def heuristic_intent(prompt: str) -> dict[str, object]:
|
|
1156
1454
|
lower = prompt.lower()
|
|
1157
|
-
memory_words = ["ghi
|
|
1158
|
-
planning_words = ["goal", "
|
|
1159
|
-
coding_words = ["
|
|
1455
|
+
memory_words = ["ghi nhớ", "remember", "note", "decision", "lưu lại", "save this"]
|
|
1456
|
+
planning_words = ["goal", "kế hoạch", "ke hoach", "milestone", "mission", "drift", "next task", "roadmap", "mục tiêu", "muc tieu"]
|
|
1457
|
+
coding_words = ["sá»a", "sua", "fix", "bug", "code", "test", "refactor", "review", "file", ".py", ".js", "endpoint", "class", "function"]
|
|
1160
1458
|
if any(word in lower for word in memory_words):
|
|
1161
1459
|
return {"intent": "memory", "confidence": 0.75}
|
|
1162
1460
|
if any(word in lower for word in planning_words):
|
|
@@ -1191,12 +1489,16 @@ def model_error_hint(message: str) -> str:
|
|
|
1191
1489
|
return message + " | Fix: /remove-model <id> or re-bind with --base-url http://localhost:8000/v1"
|
|
1192
1490
|
if "actively refused" in lower or "connection refused" in lower or "winerror 10061" in lower:
|
|
1193
1491
|
return message + " | Fix: start your OpenAI-compatible server at base_url, or change --base-url."
|
|
1492
|
+
if "http 403" in lower or "403" in lower and "groq" in lower:
|
|
1493
|
+
return message + " | Fix: Groq rejected auth/model. Rotate/check API key, ensure provider=openai-compatible, base_url=https://api.groq.com/openai/v1, and use a Groq-supported model name."
|
|
1194
1494
|
if "http 404" in lower or "404: not found" in lower:
|
|
1195
1495
|
return message + " | Fix: verify base_url includes /v1, provider supports /chat/completions, and model name exists."
|
|
1196
1496
|
if "http 400" in lower and "not supported" in lower:
|
|
1197
1497
|
return message + " | Fix: this account/provider rejects that model. Bind a supported model name from your local endpoint."
|
|
1198
1498
|
if "returned non-json" in lower or "expecting value" in lower:
|
|
1199
1499
|
return message + " | Fix: endpoint returned empty/HTML/non-JSON. Check base_url, auth key, proxy, and server logs."
|
|
1500
|
+
if "command not found" in lower or "winerror 2" in lower or "the system cannot find the file" in lower:
|
|
1501
|
+
return message + r" | Fix: install the CLI or set Main base_url to full command path. For Codex use base_url='C:\Users\admin\AppData\Roaming\npm\codex.cmd exec' or paste codex.cmd path; AgentRAM auto-adds exec when missing."
|
|
1200
1502
|
if "missing api key env" in lower:
|
|
1201
1503
|
return message + " | Fix: set environment variable, then bind using --api-key-env ENV_NAME, not raw key."
|
|
1202
1504
|
return message
|
|
@@ -1273,7 +1575,7 @@ def draw_screen(context: McpContext, messages: list[str], activity: str | None =
|
|
|
1273
1575
|
left_width = 30
|
|
1274
1576
|
right_width = width - left_width - 3
|
|
1275
1577
|
print("+" + "-" * (width - 2) + "+")
|
|
1276
|
-
print(box_text("
|
|
1578
|
+
print(box_text("AgentRAM Code | RAM + Claude subagents + slash commands", width))
|
|
1277
1579
|
print("+" + "-" * (width - 2) + "+")
|
|
1278
1580
|
|
|
1279
1581
|
sidebar = sidebar_lines(context)
|
|
@@ -1328,7 +1630,7 @@ def sidebar_lines(context: McpContext) -> list[str]:
|
|
|
1328
1630
|
|
|
1329
1631
|
|
|
1330
1632
|
def trim(text: str, limit: int) -> str:
|
|
1331
|
-
return text if len(text) <= limit else text[: limit -
|
|
1633
|
+
return text if len(text) <= limit else text[: limit - 3] + "..."
|
|
1332
1634
|
|
|
1333
1635
|
|
|
1334
1636
|
def menu_lines() -> list[str]:
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
10
12
|
import urllib.error
|
|
11
13
|
import urllib.request
|
|
12
14
|
from typing import Any, Callable
|
|
@@ -30,6 +32,12 @@ PROVIDER_ALIASES = {
|
|
|
30
32
|
"claudesubagent": "claude-subagent",
|
|
31
33
|
"claude_subagent": "claude-subagent",
|
|
32
34
|
"claude-subagent": "claude-subagent",
|
|
35
|
+
"codex": "codex-cli",
|
|
36
|
+
"codexcli": "codex-cli",
|
|
37
|
+
"codex_cli": "codex-cli",
|
|
38
|
+
"codex-code": "codex-cli",
|
|
39
|
+
"codex_cli_agent": "codex-cli",
|
|
40
|
+
"codex-cli": "codex-cli",
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
|
|
@@ -38,6 +46,38 @@ def normalize_provider_name(provider: str) -> str:
|
|
|
38
46
|
return PROVIDER_ALIASES.get(normalized, normalized)
|
|
39
47
|
|
|
40
48
|
|
|
49
|
+
def split_cli_command(command: str) -> list[str]:
|
|
50
|
+
cleaned = command.strip().strip('"')
|
|
51
|
+
if not cleaned:
|
|
52
|
+
return []
|
|
53
|
+
if os.name == "nt":
|
|
54
|
+
lowered = cleaned.lower()
|
|
55
|
+
for marker in (".cmd", ".exe", ".bat", ".ps1"):
|
|
56
|
+
index = lowered.find(marker)
|
|
57
|
+
if index != -1:
|
|
58
|
+
end = index + len(marker)
|
|
59
|
+
program = cleaned[:end]
|
|
60
|
+
rest = cleaned[end:].strip()
|
|
61
|
+
return [program, *shlex.split(rest)] if rest else [program]
|
|
62
|
+
return shlex.split(cleaned)
|
|
63
|
+
|
|
64
|
+
def env_value(name: str) -> str:
|
|
65
|
+
value = os.getenv(name, "")
|
|
66
|
+
if value:
|
|
67
|
+
return value
|
|
68
|
+
env_path = Path(".env")
|
|
69
|
+
if not env_path.exists():
|
|
70
|
+
return ""
|
|
71
|
+
for line in env_path.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
72
|
+
stripped = line.strip()
|
|
73
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
74
|
+
continue
|
|
75
|
+
key, raw_value = stripped.split("=", 1)
|
|
76
|
+
if key.strip() == name:
|
|
77
|
+
return raw_value.strip().strip('"').strip("'")
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
|
|
41
81
|
@dataclass(frozen=True)
|
|
42
82
|
class AgentModelEndpoint:
|
|
43
83
|
provider: str
|
|
@@ -137,10 +177,12 @@ class MultiModelCodingOrchestrator:
|
|
|
137
177
|
return self._call_anthropic(endpoint, prompt)
|
|
138
178
|
if provider in {"claude-code", "claude-cli", "claude-subagent", "claude-code-subagent"}:
|
|
139
179
|
return self._call_claude_code_subagent(endpoint, prompt)
|
|
180
|
+
if provider in {"codex-cli"}:
|
|
181
|
+
return self._call_codex_cli_agent(endpoint, prompt)
|
|
140
182
|
raise ValueError(f"Unsupported provider for execution: {endpoint.provider}")
|
|
141
183
|
|
|
142
184
|
def _call_openai_compatible(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
143
|
-
api_key =
|
|
185
|
+
api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
|
|
144
186
|
if not endpoint.base_url:
|
|
145
187
|
raise ValueError("base_url is required")
|
|
146
188
|
if endpoint.api_key_env and not api_key:
|
|
@@ -162,17 +204,20 @@ class MultiModelCodingOrchestrator:
|
|
|
162
204
|
if error.code == 404:
|
|
163
205
|
hint += " | base_url should include /v1 but not /chat/completions; verify provider URL and model name"
|
|
164
206
|
raise RuntimeError(f"{hint} | response={body[:500]}") from error
|
|
165
|
-
try:
|
|
166
|
-
data = json.loads(raw)
|
|
167
|
-
except json.JSONDecodeError as error:
|
|
168
|
-
|
|
169
|
-
if
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
207
|
+
try:
|
|
208
|
+
data = json.loads(raw)
|
|
209
|
+
except json.JSONDecodeError as error:
|
|
210
|
+
mixed_json = parse_openai_mixed_json(raw)
|
|
211
|
+
if mixed_json:
|
|
212
|
+
return extract_openai_message(mixed_json)
|
|
213
|
+
streamed = parse_openai_sse(raw)
|
|
214
|
+
if streamed:
|
|
215
|
+
return streamed
|
|
216
|
+
raise RuntimeError(f"OpenAI-compatible returned non-JSON at {url} | response={raw[:500]}") from error
|
|
217
|
+
return extract_openai_message(data)
|
|
173
218
|
|
|
174
219
|
def _call_anthropic(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
175
|
-
api_key =
|
|
220
|
+
api_key = env_value(endpoint.api_key_env) if endpoint.api_key_env else ""
|
|
176
221
|
if endpoint.api_key_env and not api_key:
|
|
177
222
|
raise ValueError(f"missing API key env: {endpoint.api_key_env}")
|
|
178
223
|
base_url = endpoint.base_url.rstrip("/") if endpoint.base_url else "https://api.anthropic.com/v1"
|
|
@@ -192,7 +237,7 @@ class MultiModelCodingOrchestrator:
|
|
|
192
237
|
|
|
193
238
|
def _call_claude_code_subagent(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
194
239
|
command = endpoint.base_url.strip() or os.getenv("AGENTRAM_CLAUDE_COMMAND", "claude")
|
|
195
|
-
args =
|
|
240
|
+
args = split_cli_command(command)
|
|
196
241
|
if not args:
|
|
197
242
|
raise ValueError("claude command is required")
|
|
198
243
|
subagent_prompt = "\n".join(
|
|
@@ -200,25 +245,81 @@ class MultiModelCodingOrchestrator:
|
|
|
200
245
|
f"Use Claude Code subagent entrypoint: {endpoint.model}",
|
|
201
246
|
f"Subagent role: {endpoint.role}",
|
|
202
247
|
"If that subagent exists in .claude/agents, follow it. If not, act as this role.",
|
|
203
|
-
"
|
|
248
|
+
"If role is coder/main, you may inspect and edit files using Claude Code tools. If role is memory/small, do not edit files; write notes only.",
|
|
249
|
+
"Return concise coding-agent output, changed files, and verification status.",
|
|
204
250
|
"",
|
|
205
251
|
prompt,
|
|
206
252
|
]
|
|
207
253
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
try:
|
|
255
|
+
process = subprocess.run(
|
|
256
|
+
[*args, "-p", subagent_prompt],
|
|
257
|
+
check=False,
|
|
258
|
+
capture_output=True,
|
|
259
|
+
encoding="utf-8",
|
|
260
|
+
timeout=300,
|
|
261
|
+
)
|
|
262
|
+
except FileNotFoundError as error:
|
|
263
|
+
raise RuntimeError(f"Claude Code command not found: {args[0]}. Install Claude Code CLI or set base_url to the full command path.") from error
|
|
215
264
|
output = (process.stdout or "").strip()
|
|
216
265
|
error = (process.stderr or "").strip()
|
|
217
266
|
if process.returncode != 0:
|
|
218
267
|
raise RuntimeError(error or f"claude command failed with exit code {process.returncode}")
|
|
219
268
|
return output or error
|
|
220
269
|
|
|
221
|
-
def
|
|
270
|
+
def _call_codex_cli_agent(self, endpoint: AgentModelEndpoint, prompt: str) -> str:
|
|
271
|
+
command = endpoint.base_url.strip() or os.getenv("AGENTRAM_CODEX_COMMAND", "codex exec")
|
|
272
|
+
args = split_cli_command(command)
|
|
273
|
+
if not args:
|
|
274
|
+
raise ValueError("codex command is required")
|
|
275
|
+
if len(args) == 1:
|
|
276
|
+
args.append("exec")
|
|
277
|
+
codex_prompt = "\n".join(
|
|
278
|
+
[
|
|
279
|
+
f"You are AgentRAM {endpoint.role} agent running through Codex CLI.",
|
|
280
|
+
f"Model hint: {endpoint.model}",
|
|
281
|
+
"If role is coder/main, inspect and edit files as needed in the current repo. If role is memory/small, do not edit files; write notes only.",
|
|
282
|
+
"Return concise result, changed files, tests/verification, and blockers.",
|
|
283
|
+
"",
|
|
284
|
+
prompt,
|
|
285
|
+
]
|
|
286
|
+
)
|
|
287
|
+
prompt_path = ""
|
|
288
|
+
try:
|
|
289
|
+
with tempfile.NamedTemporaryFile(
|
|
290
|
+
"w",
|
|
291
|
+
encoding="utf-8",
|
|
292
|
+
delete=False,
|
|
293
|
+
dir=os.getcwd(),
|
|
294
|
+
prefix=".agentram_codex_prompt_",
|
|
295
|
+
suffix=".txt",
|
|
296
|
+
) as prompt_file:
|
|
297
|
+
prompt_file.write(codex_prompt)
|
|
298
|
+
prompt_path = prompt_file.name
|
|
299
|
+
short_prompt = f"Read AgentRAM task from file and execute it: {prompt_path}"
|
|
300
|
+
process = subprocess.run(
|
|
301
|
+
[*args, short_prompt],
|
|
302
|
+
check=False,
|
|
303
|
+
capture_output=True,
|
|
304
|
+
encoding="utf-8",
|
|
305
|
+
timeout=int(os.getenv("AGENTRAM_CLI_TIMEOUT", "900")),
|
|
306
|
+
)
|
|
307
|
+
except FileNotFoundError as error:
|
|
308
|
+
raise RuntimeError(f"Codex CLI command not found: {args[0]}. Install Codex CLI or set base_url to the full command path.") from error
|
|
309
|
+
finally:
|
|
310
|
+
if prompt_path:
|
|
311
|
+
try:
|
|
312
|
+
os.remove(prompt_path)
|
|
313
|
+
except OSError:
|
|
314
|
+
pass
|
|
315
|
+
output = (process.stdout or "").strip()
|
|
316
|
+
error = (process.stderr or "").strip()
|
|
317
|
+
if process.returncode != 0:
|
|
318
|
+
raise RuntimeError(error or f"codex command failed with exit code {process.returncode}")
|
|
319
|
+
return output or error
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def parse_openai_sse(raw: str) -> str:
|
|
222
323
|
parts: list[str] = []
|
|
223
324
|
for line in raw.splitlines():
|
|
224
325
|
stripped = line.strip()
|
|
@@ -245,9 +346,39 @@ def parse_openai_sse(raw: str) -> str:
|
|
|
245
346
|
message = choice.get("message", {})
|
|
246
347
|
if isinstance(message, dict) and message.get("content"):
|
|
247
348
|
parts.append(str(message.get("content")))
|
|
248
|
-
return strip_thinking_blocks("".join(parts)).strip()
|
|
249
|
-
|
|
250
|
-
def
|
|
349
|
+
return strip_thinking_blocks("".join(parts)).strip()
|
|
350
|
+
|
|
351
|
+
def parse_openai_mixed_json(raw: str) -> dict[str, Any]:
|
|
352
|
+
stripped = raw.strip()
|
|
353
|
+
if not stripped.startswith("{"):
|
|
354
|
+
return {}
|
|
355
|
+
for marker in ("\ndata:", "\r\ndata:"):
|
|
356
|
+
if marker not in stripped:
|
|
357
|
+
continue
|
|
358
|
+
candidate = stripped.split(marker, 1)[0].strip()
|
|
359
|
+
try:
|
|
360
|
+
value = json.loads(candidate)
|
|
361
|
+
except json.JSONDecodeError:
|
|
362
|
+
return {}
|
|
363
|
+
return value if isinstance(value, dict) else {}
|
|
364
|
+
return {}
|
|
365
|
+
|
|
366
|
+
def extract_openai_message(data: dict[str, Any]) -> str:
|
|
367
|
+
choices = data.get("choices", [])
|
|
368
|
+
if not choices or not isinstance(choices, list):
|
|
369
|
+
return ""
|
|
370
|
+
first = choices[0]
|
|
371
|
+
if not isinstance(first, dict):
|
|
372
|
+
return ""
|
|
373
|
+
message = first.get("message", {})
|
|
374
|
+
if isinstance(message, dict):
|
|
375
|
+
return strip_thinking_blocks(str(message.get("content", ""))).strip()
|
|
376
|
+
delta = first.get("delta", {})
|
|
377
|
+
if isinstance(delta, dict):
|
|
378
|
+
return strip_thinking_blocks(str(delta.get("content", ""))).strip()
|
|
379
|
+
return ""
|
|
380
|
+
|
|
381
|
+
def strip_thinking_blocks(text: str) -> str:
|
|
251
382
|
text = re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
252
383
|
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
253
384
|
text = re.sub(r"^\s*</?(thinking|think)>\s*", "", text, flags=re.IGNORECASE)
|
package/package.json
CHANGED